"""
Comprehensive model testing suite.
Tests all models, relationships, properties, and business logic.
"""

import pytest
from datetime import datetime, timedelta, date
from decimal import Decimal

from app.models import User, Project, TimeEntry, Client, Settings, Invoice, InvoiceItem, Task
from factories import InvoiceFactory
from app import db


# ============================================================================
# User Model Tests
# ============================================================================


@pytest.mark.unit
@pytest.mark.models
@pytest.mark.smoke
def test_user_creation(app, user):
    """Test basic user creation."""
    assert user.id is not None
    assert user.username == "testuser"
    assert user.role == "user"
    assert user.is_active is True


@pytest.mark.unit
@pytest.mark.models
def test_user_is_admin_property(app, admin_user):
    """Test user is_admin property."""
    assert admin_user.is_admin is True


@pytest.mark.unit
@pytest.mark.models
def test_user_active_timer(app, user, active_timer):
    """Test user active_timer property."""
    # Refresh user to load relationships
    db.session.refresh(user)
    assert user.active_timer is not None
    assert user.active_timer.id == active_timer.id


@pytest.mark.unit
@pytest.mark.models
def test_user_time_entries_relationship(app, user, multiple_time_entries):
    """Test user time entries relationship."""
    db.session.refresh(user)
    assert len(user.time_entries.all()) == 5


@pytest.mark.unit
@pytest.mark.models
def test_user_to_dict(app, user):
    """Test user serialization to dictionary."""
    user_dict = user.to_dict()
    assert "id" in user_dict
    assert "username" in user_dict
    assert "role" in user_dict
    # Should not include sensitive data
    assert "password" not in user_dict


# ============================================================================
# Client Model Tests
# ============================================================================


@pytest.mark.unit
@pytest.mark.models
@pytest.mark.smoke
def test_client_creation(app, test_client):
    """Test basic client creation."""
    assert test_client.id is not None
    assert test_client.name == "Test Client Corp"
    assert test_client.status == "active"
    assert test_client.default_hourly_rate == Decimal("85.00")


@pytest.mark.unit
@pytest.mark.models
def test_client_projects_relationship(app, test_client, multiple_projects):
    """Test client projects relationship."""
    db.session.refresh(test_client)
    assert len(test_client.projects.all()) == 3


@pytest.mark.unit
@pytest.mark.models
def test_client_total_projects_property(app, test_client, multiple_projects):
    """Test client total_projects property."""
    db.session.refresh(test_client)
    assert test_client.total_projects == 3


@pytest.mark.unit
@pytest.mark.models
def test_client_archive_activate(app, test_client):
    """Test client archive and activate methods."""
    db.session.refresh(test_client)

    # Archive client
    test_client.archive()
    db.session.commit()
    assert test_client.status == "inactive"

    # Activate client
    test_client.activate()
    db.session.commit()
    assert test_client.status == "active"


@pytest.mark.unit
@pytest.mark.models
def test_client_get_active_clients(app, multiple_clients):
    """Test get_active_clients class method."""
    active_clients = Client.get_active_clients()
    assert len(active_clients) >= 3


@pytest.mark.unit
@pytest.mark.models
def test_client_to_dict(app, test_client):
    """Test client serialization to dictionary."""
    client_dict = test_client.to_dict()
    assert "id" in client_dict
    assert "name" in client_dict
    assert "status" in client_dict


# ============================================================================
# Project Model Tests
# ============================================================================


@pytest.mark.unit
@pytest.mark.models
@pytest.mark.smoke
def test_project_creation(app, project):
    """Test basic project creation."""
    assert project.id is not None
    assert project.name == "Test Project"
    assert project.billable is True
    assert project.status == "active"


@pytest.mark.unit
@pytest.mark.models
def test_project_client_relationship(app, project, test_client):
    """Test project client relationship."""
    db.session.refresh(project)
    db.session.refresh(test_client)
    assert project.client_id == test_client.id
    # Check backward compatibility
    if hasattr(project, "client"):
        assert project.client == test_client.name


@pytest.mark.unit
@pytest.mark.models
def test_project_time_entries_relationship(app, project, multiple_time_entries):
    """Test project time entries relationship."""
    db.session.refresh(project)
    assert len(project.time_entries.all()) == 5


@pytest.mark.unit
@pytest.mark.models
def test_project_total_hours(app, project, multiple_time_entries):
    """Test project total_hours property."""
    db.session.refresh(project)
    # Each entry is 8 hours (9am to 5pm), 5 entries = 40 hours
    assert project.total_hours > 0


@pytest.mark.unit
@pytest.mark.models
def test_project_estimated_cost(app, project, multiple_time_entries):
    """Test project estimated_cost property."""
    db.session.refresh(project)
    estimated_cost = project.estimated_cost
    assert estimated_cost > 0
    # Cost should be hours * hourly_rate
    expected_cost = project.total_hours * float(project.hourly_rate)
    assert abs(float(estimated_cost) - expected_cost) < 0.01


@pytest.mark.unit
@pytest.mark.models
def test_project_archive(app, project):
    """Test project archiving."""
    db.session.refresh(project)
    project.status = "archived"
    db.session.commit()
    assert project.status == "archived"


# ============================================================================
# Time Entry Model Tests
# ============================================================================


@pytest.mark.unit
@pytest.mark.models
@pytest.mark.smoke
def test_time_entry_creation(app, time_entry):
    """Test basic time entry creation."""
    assert time_entry.id is not None
    assert time_entry.start_time is not None
    assert time_entry.end_time is not None


@pytest.mark.unit
@pytest.mark.models
def test_time_entry_duration(app, time_entry):
    """Test time entry duration calculations."""
    db.session.refresh(time_entry)
    assert time_entry.duration_seconds > 0
    assert time_entry.duration_hours > 0
    assert time_entry.duration_formatted is not None


@pytest.mark.unit
@pytest.mark.models
def test_active_timer_is_active(app, active_timer):
    """Test active timer is_active property."""
    db.session.refresh(active_timer)
    assert active_timer.is_active is True
    assert active_timer.end_time is None


@pytest.mark.unit
@pytest.mark.models
def test_stop_timer(app, active_timer):
    """Test stopping an active timer."""
    db.session.refresh(active_timer)
    active_timer.stop_timer()
    db.session.commit()

    db.session.refresh(active_timer)
    assert active_timer.is_active is False
    assert active_timer.end_time is not None
    assert active_timer.duration_seconds > 0


@pytest.mark.unit
@pytest.mark.models
def test_time_entry_tag_list(app, test_client):
    """Test time entry tag_list property."""
    from app.models import User, Project

    user = User.query.first() or User(username="test", role="user")
    project = Project.query.first() or Project(name="Test", client_id=test_client.id, billable=True)

    if not user.id:
        db.session.add(user)
    if not project.id:
        db.session.add(project)
    db.session.commit()

    from factories import TimeEntryFactory

    entry = TimeEntryFactory(
        user_id=user.id,
        project_id=project.id,
        start_time=datetime.utcnow(),
        end_time=datetime.utcnow() + timedelta(hours=1),
        tags="python,testing,development",
        source="manual",
    )

    db.session.refresh(entry)
    assert entry.tag_list == ["python", "testing", "development"]


# ============================================================================
# Task Model Tests
# ============================================================================


@pytest.mark.unit
@pytest.mark.models
def test_task_creation(app, task):
    """Test basic task creation."""
    db.session.refresh(task)
    assert task.id is not None
    assert task.name == "Test Task"
    assert task.status == "todo"


@pytest.mark.unit
@pytest.mark.models
def test_task_project_relationship(app, task, project):
    """Test task project relationship."""
    db.session.refresh(task)
    db.session.refresh(project)
    assert task.project_id == project.id


@pytest.mark.unit
@pytest.mark.models
def test_task_status_transitions(app, task):
    """Test task status transitions."""
    db.session.refresh(task)

    # Mark as in progress
    task.status = "in_progress"
    db.session.commit()
    assert task.status == "in_progress"

    # Mark as done
    task.status = "done"
    db.session.commit()
    assert task.status == "done"


# ============================================================================
# Invoice Model Tests
# ============================================================================


@pytest.mark.unit
@pytest.mark.models
@pytest.mark.smoke
def test_invoice_creation(app, invoice):
    """Test basic invoice creation."""
    # Invoice is already refreshed in the fixture, no need to refresh again
    assert invoice.id is not None
    assert invoice.invoice_number is not None
    assert invoice.status == "draft"


@pytest.mark.unit
@pytest.mark.models
def test_invoice_number_generation(app):
    """Test invoice number generation."""
    invoice_number = Invoice.generate_invoice_number()
    assert invoice_number is not None
    assert "INV-" in invoice_number


@pytest.mark.unit
@pytest.mark.models
def test_invoice_calculate_totals(app, invoice_with_items):
    """Test invoice total calculations."""
    invoice, items = invoice_with_items

    # Invoice is already committed and refreshed in the fixture
    # 10 * 75 + 5 * 60 = 750 + 300 = 1050
    assert invoice.subtotal == Decimal("1050.00")

    # Tax: 20% of 1050 = 210
    assert invoice.tax_amount == Decimal("210.00")

    # Total: 1050 + 210 = 1260
    assert invoice.total_amount == Decimal("1260.00")


@pytest.mark.unit
@pytest.mark.models
def test_invoice_payment_tracking(app, invoice_with_items):
    """Test invoice payment tracking."""
    invoice, items = invoice_with_items

    # Record partial payment
    partial_payment = invoice.total_amount / 2
    invoice.record_payment(
        amount=partial_payment, payment_date=date.today(), payment_method="bank_transfer", payment_reference="TEST-123"
    )
    db.session.commit()

    db.session.expire(invoice)
    db.session.refresh(invoice)
    assert invoice.payment_status == "partially_paid"
    assert invoice.amount_paid == partial_payment
    assert invoice.is_partially_paid is True

    # Record remaining payment
    remaining = invoice.outstanding_amount
    invoice.record_payment(amount=remaining, payment_method="bank_transfer")
    db.session.commit()

    db.session.expire(invoice)
    db.session.refresh(invoice)
    assert invoice.payment_status == "fully_paid"
    assert invoice.is_paid is True
    assert invoice.outstanding_amount == Decimal("0")


@pytest.mark.unit
@pytest.mark.models
def test_invoice_overdue_status(app, user, project, test_client):
    """Test invoice overdue status."""
    # Create overdue invoice
    overdue_invoice = InvoiceFactory(
        invoice_number=Invoice.generate_invoice_number(),
        project_id=project.id,
        client_id=test_client.id,
        client_name="Test Client",
        due_date=date.today() - timedelta(days=10),
        created_by=user.id,
        status="sent",
    )
    db.session.commit()

    # Ensure status is 'sent' for overdue calculation compatibility
    overdue_invoice.status = "sent"
    db.session.commit()
    db.session.refresh(overdue_invoice)
    assert overdue_invoice.is_overdue is True
    assert overdue_invoice.days_overdue == 10


# ============================================================================
# Settings Model Tests
# ============================================================================


@pytest.mark.unit
@pytest.mark.models
def test_settings_singleton(app):
    """Test settings singleton pattern."""
    settings1 = Settings.get_settings()
    settings2 = Settings.get_settings()

    assert settings1.id == settings2.id


@pytest.mark.unit
@pytest.mark.models
def test_settings_default_values(app):
    """Test settings default values."""
    settings = Settings.get_settings()

    # Check that settings has expected attributes
    assert hasattr(settings, "id")
    # Add more default value checks based on your Settings model


# ============================================================================
# Model Relationship Tests
# ============================================================================


@pytest.mark.integration
@pytest.mark.models
@pytest.mark.database
def test_cascade_delete_user_time_entries(app, user, multiple_time_entries):
    """Test cascade delete of user time entries."""
    user_id = user.id

    # Get time entry count
    entry_count = TimeEntry.query.filter_by(user_id=user_id).count()
    assert entry_count == 5

    # Delete user
    db.session.delete(user)
    db.session.commit()

    # Check time entries are deleted or handled
    remaining_entries = TimeEntry.query.filter_by(user_id=user_id).count()
    # Depending on cascade settings, entries might be deleted or set to null
    # For now, we just verify the operation completed without errors
    assert remaining_entries >= 0  # Operation completed successfully


@pytest.mark.integration
@pytest.mark.models
@pytest.mark.database
def test_project_client_relationship_integrity(app, project, test_client):
    """Test project-client relationship integrity."""
    # Verify the relationship
    assert project.client_id == test_client.id

    # Get project through client relationship
    client_projects = Client.query.get(test_client.id).projects.all()
    project_ids = [p.id for p in client_projects]
    assert project.id in project_ids


# ============================================================================
# Model Validation Tests
# ============================================================================


@pytest.mark.unit
@pytest.mark.models
def test_project_requires_name(app, test_client):
    """Test that project requires a name."""
    # Project __init__ requires name as first positional argument
    # This test verifies the API enforces this requirement
    with pytest.raises(TypeError):
        project = Project(billable=True)


@pytest.mark.unit
@pytest.mark.models
def test_time_entry_requires_start_time(app, user, project):
    """Test that time entry requires start time."""
    # TimeEntry requires start_time at database level (nullable=False)
    # This test verifies the database enforces this requirement
    from sqlalchemy.exc import IntegrityError
    from app import db

    with pytest.raises(IntegrityError):
        entry = TimeEntry(user_id=user.id, project_id=project.id, source="manual")
        db.session.add(entry)
        db.session.commit()


# ============================================================================
# User Deletion and Cascading Tests
# ============================================================================


@pytest.mark.unit
@pytest.mark.models
def test_user_deletion_without_relationships(app):
    """Test that a user without relationships can be deleted."""
    with app.app_context():
        # Create a user with no relationships
        delete_user = User(username="deletable", role="user")
        delete_user.is_active = True
        db.session.add(delete_user)
        db.session.commit()
        user_id = delete_user.id

        # Delete the user
        db.session.delete(delete_user)
        db.session.commit()

        # Verify deletion
        deleted = User.query.get(user_id)
        assert deleted is None


@pytest.mark.unit
@pytest.mark.models
def test_user_deletion_cascades_project_costs(app, test_client):
    """Test that deleting a user cascades to project costs."""
    from app.models import ProjectCost
    from datetime import date

    with app.app_context():
        # Create user and project
        user = User(username="costuser", role="user")
        user.is_active = True
        db.session.add(user)

        project = Project(name="Cost Test Project", client_id=test_client.id, billable=True)
        db.session.add(project)
        db.session.commit()

        # Create project cost
        cost = ProjectCost(
            project_id=project.id,
            user_id=user.id,
            description="Test expense",
            category="materials",
            amount=Decimal("100.00"),
            cost_date=date.today(),
        )
        db.session.add(cost)
        db.session.commit()

        user_id = user.id
        cost_id = cost.id

        # Delete user
        db.session.delete(user)
        db.session.commit()

        # Verify user is deleted
        deleted_user = User.query.get(user_id)
        assert deleted_user is None

        # Verify project cost is cascaded (deleted)
        deleted_cost = ProjectCost.query.get(cost_id)
        assert deleted_cost is None


@pytest.mark.unit
@pytest.mark.models
def test_user_deletion_cascades_time_entries(app, test_client):
    """Test that deleting a user cascades to time entries."""
    with app.app_context():
        # Create user and project
        user = User(username="entryuser", role="user")
        user.is_active = True
        db.session.add(user)

        project = Project(name="Entry Test Project", client_id=test_client.id, billable=True)
        db.session.add(project)
        db.session.commit()

        # Create time entry
        entry = TimeEntry(
            user_id=user.id,
            project_id=project.id,
            start_time=datetime.utcnow(),
            end_time=datetime.utcnow() + timedelta(hours=1),
            description="Test entry",
        )
        db.session.add(entry)
        db.session.commit()

        user_id = user.id
        entry_id = entry.id

        # Delete user
        db.session.delete(user)
        db.session.commit()

        # Verify user is deleted
        deleted_user = User.query.get(user_id)
        assert deleted_user is None

        # Verify time entry is cascaded (deleted)
        deleted_entry = TimeEntry.query.get(entry_id)
        assert deleted_entry is None


@pytest.mark.unit
@pytest.mark.models
def test_user_deletion_removes_from_favorite_projects(app, test_client):
    """Test that deleting a user removes them from favorite projects."""
    with app.app_context():
        # Create user and project
        user = User(username="favuser", role="user")
        user.is_active = True
        db.session.add(user)

        project = Project(name="Favorite Test Project", client_id=test_client.id, billable=True)
        db.session.add(project)
        db.session.commit()

        # Add project to favorites
        user.favorite_projects.append(project)
        db.session.commit()

        # Verify favorite was added
        assert project in user.favorite_projects.all()

        user_id = user.id
        project_id = project.id

        # Delete user
        db.session.delete(user)
        db.session.commit()

        # Verify user is deleted
        deleted_user = User.query.get(user_id)
        assert deleted_user is None

        # Verify project still exists (favorites are many-to-many)
        remaining_project = Project.query.get(project_id)
        assert remaining_project is not None

        # Verify user is not in project's favorited_by
        assert user_id not in [u.id for u in remaining_project.favorited_by.all()]


@pytest.mark.unit
@pytest.mark.models
def test_user_deletion_preserves_tasks_assigned_to_them(app, test_client):
    """Test that deleting a user preserves tasks but nullifies assigned_to."""
    with app.app_context():
        # Create users and project
        creator = User(username="creator", role="user")
        creator.is_active = True
        assignee = User(username="assignee", role="user")
        assignee.is_active = True
        db.session.add_all([creator, assignee])

        project = Project(name="Task Test Project", client_id=test_client.id, billable=True)
        db.session.add(project)
        db.session.commit()

        # Create task
        task = Task(
            project_id=project.id,
            name="Test Task",
            description="Test description",
            created_by=creator.id,
            assigned_to=assignee.id,
        )
        db.session.add(task)
        db.session.commit()

        assignee_id = assignee.id
        task_id = task.id

        # Delete assignee
        db.session.delete(assignee)
        db.session.commit()

        # Verify assignee is deleted
        deleted_user = User.query.get(assignee_id)
        assert deleted_user is None

        # Verify task still exists but assigned_to is nullified
        remaining_task = Task.query.get(task_id)
        assert remaining_task is not None
        assert remaining_task.assigned_to is None


@pytest.mark.unit
@pytest.mark.models
def test_user_cannot_be_deleted_if_has_created_tasks(app, test_client):
    """Test that deleting a user who created tasks cascades properly."""
    from sqlalchemy.exc import IntegrityError

    with app.app_context():
        # Create user and project
        creator = User(username="taskcreator", role="user")
        creator.is_active = True
        db.session.add(creator)

        project = Project(name="Task Creator Project", client_id=test_client.id, billable=True)
        db.session.add(project)
        db.session.commit()

        # Create task
        task = Task(project_id=project.id, name="Created Task", description="Test description", created_by=creator.id)
        db.session.add(task)
        db.session.commit()

        creator_id = creator.id

        # Try to delete creator - should raise IntegrityError because created_by is NOT NULL
        with pytest.raises(IntegrityError):
            db.session.delete(creator)
            db.session.commit()

        db.session.rollback()

        # Verify creator still exists
        still_exists = User.query.get(creator_id)
        assert still_exists is not None


@pytest.mark.unit
@pytest.mark.models
def test_user_deletion_count_check(app):
    """Test that we can query user count before and after deletion."""
    with app.app_context():
        # Get initial count
        initial_count = User.query.count()

        # Create and delete a user
        temp_user = User(username="tempuser", role="user")
        temp_user.is_active = True
        db.session.add(temp_user)
        db.session.commit()

        # Verify count increased
        assert User.query.count() == initial_count + 1

        # Delete user
        db.session.delete(temp_user)
        db.session.commit()

        # Verify count back to initial
        assert User.query.count() == initial_count
