"""
Unit tests for Project domain models.

Tests pure business logic with no external dependencies.
"""

import pytest
from datetime import datetime, timedelta, timezone

from domain.projects.models import Project, Milestone, ProjectState
from domain.projects.exceptions import MilestoneError


class TestMilestone:
    """Test Milestone entity."""

    def test_milestone_creation(self):
        # Arrange
        due_date = datetime.now(timezone.utc) + timedelta(days=30)

        # Act
        milestone = Milestone(
            id=1,
            title="Version 1.0",
            description="First release",
            due_date=due_date,
            state="open",
            open_issues=5,
            closed_issues=3
        )

        # Assert
        assert milestone.id == 1
        assert milestone.title == "Version 1.0"
        assert milestone.description == "First release"
        assert milestone.due_date == due_date
        assert milestone.state == "open"
        assert milestone.open_issues == 5
        assert milestone.closed_issues == 3

    def test_completion_percentage_calculation(self):
        # Arrange
        milestone = Milestone(
            id=1,
            title="Test",
            description=None,
            due_date=None,
            state="open",
            open_issues=2,
            closed_issues=8
        )

        # Act
        percentage = milestone.completion_percentage

        # Assert
        assert percentage == 80.0  # 8/(2+8) * 100

    def test_completion_percentage_with_no_issues(self):
        # Arrange
        milestone = Milestone(
            id=1,
            title="Test",
            description=None,
            due_date=None,
            state="open",
            open_issues=0,
            closed_issues=0
        )

        # Act
        percentage = milestone.completion_percentage

        # Assert
        assert percentage == 0.0

    def test_total_issues_property(self):
        # Arrange
        milestone = Milestone(
            id=1,
            title="Test",
            description=None,
            due_date=None,
            state="open",
            open_issues=3,
            closed_issues=7
        )

        # Act & Assert
        assert milestone.total_issues == 10

    def test_is_overdue_with_past_due_date(self):
        # Arrange
        past_date = datetime.now(timezone.utc) - timedelta(days=1)
        milestone = Milestone(
            id=1,
            title="Test",
            description=None,
            due_date=past_date,
            state="open",
            open_issues=1,
            closed_issues=0
        )

        # Act & Assert
        assert milestone.is_overdue() is True

    def test_is_overdue_with_future_due_date(self):
        # Arrange
        future_date = datetime.now(timezone.utc) + timedelta(days=1)
        milestone = Milestone(
            id=1,
            title="Test",
            description=None,
            due_date=future_date,
            state="open",
            open_issues=1,
            closed_issues=0
        )

        # Act & Assert
        assert milestone.is_overdue() is False

    def test_is_overdue_with_no_due_date(self):
        # Arrange
        milestone = Milestone(
            id=1,
            title="Test",
            description=None,
            due_date=None,
            state="open",
            open_issues=1,
            closed_issues=0
        )

        # Act & Assert
        assert milestone.is_overdue() is False

    def test_is_overdue_with_closed_milestone(self):
        # Arrange
        past_date = datetime.now(timezone.utc) - timedelta(days=1)
        milestone = Milestone(
            id=1,
            title="Test",
            description=None,
            due_date=past_date,
            state="closed",
            open_issues=0,
            closed_issues=5
        )

        # Act & Assert
        assert milestone.is_overdue() is False

    def test_is_completed_with_closed_state(self):
        # Arrange
        milestone = Milestone(
            id=1,
            title="Test",
            description=None,
            due_date=None,
            state="closed",
            open_issues=0,
            closed_issues=5
        )

        # Act & Assert
        assert milestone.is_completed() is True

    def test_is_completed_with_100_percent_completion(self):
        # Arrange
        milestone = Milestone(
            id=1,
            title="Test",
            description=None,
            due_date=None,
            state="open",
            open_issues=0,
            closed_issues=5
        )

        # Act & Assert
        assert milestone.is_completed() is True

    def test_is_completed_with_partial_completion(self):
        # Arrange
        milestone = Milestone(
            id=1,
            title="Test",
            description=None,
            due_date=None,
            state="open",
            open_issues=2,
            closed_issues=3
        )

        # Act & Assert
        assert milestone.is_completed() is False

    def test_add_issue_increments_open_count(self):
        # Arrange
        milestone = Milestone(
            id=1,
            title="Test",
            description=None,
            due_date=None,
            state="open",
            open_issues=3,
            closed_issues=2
        )

        # Act
        milestone.add_issue()

        # Assert
        assert milestone.open_issues == 4
        assert milestone.closed_issues == 2

    def test_close_issue_moves_from_open_to_closed(self):
        # Arrange
        milestone = Milestone(
            id=1,
            title="Test",
            description=None,
            due_date=None,
            state="open",
            open_issues=3,
            closed_issues=2
        )

        # Act
        milestone.close_issue()

        # Assert
        assert milestone.open_issues == 2
        assert milestone.closed_issues == 3

    def test_close_issue_with_no_open_issues_raises_error(self):
        # Arrange
        milestone = Milestone(
            id=1,
            title="Test",
            description=None,
            due_date=None,
            state="open",
            open_issues=0,
            closed_issues=5
        )

        # Act & Assert
        with pytest.raises(MilestoneError) as exc_info:
            milestone.close_issue()

        assert "no open issues" in str(exc_info.value)
        assert exc_info.value.milestone_id == 1

    def test_reopen_issue_moves_from_closed_to_open(self):
        # Arrange
        milestone = Milestone(
            id=1,
            title="Test",
            description=None,
            due_date=None,
            state="open",
            open_issues=2,
            closed_issues=3
        )

        # Act
        milestone.reopen_issue()

        # Assert
        assert milestone.open_issues == 3
        assert milestone.closed_issues == 2

    def test_reopen_issue_with_no_closed_issues_raises_error(self):
        # Arrange
        milestone = Milestone(
            id=1,
            title="Test",
            description=None,
            due_date=None,
            state="open",
            open_issues=5,
            closed_issues=0
        )

        # Act & Assert
        with pytest.raises(MilestoneError) as exc_info:
            milestone.reopen_issue()

        assert "no closed issues" in str(exc_info.value)
        assert exc_info.value.milestone_id == 1


class TestProject:
    """Test Project aggregate root."""

    def test_project_creation(self):
        # Arrange
        created_at = datetime.now(timezone.utc)
        updated_at = datetime.now(timezone.utc)
        milestones = [
            Milestone(1, "M1", None, None, "open", 2, 1),
            Milestone(2, "M2", None, None, "closed", 0, 3)
        ]

        # Act
        project = Project(
            name="Test Project",
            description="A test project",
            state=ProjectState.ACTIVE,
            milestones=milestones,
            kanban_columns=["Todo", "In Progress", "Done"],
            created_at=created_at,
            updated_at=updated_at
        )

        # Assert
        assert project.name == "Test Project"
        assert project.description == "A test project"
        assert project.state == ProjectState.ACTIVE
        assert len(project.milestones) == 2
        assert project.kanban_columns == ["Todo", "In Progress", "Done"]

    def test_get_active_milestones(self):
        # Arrange
        milestones = [
            Milestone(1, "M1", None, None, "open", 2, 1),
            Milestone(2, "M2", None, None, "closed", 0, 3),
            Milestone(3, "M3", None, None, "open", 1, 0)
        ]
        project = Project(
            name="Test",
            description="",
            state=ProjectState.ACTIVE,
            milestones=milestones,
            kanban_columns=[],
            created_at=datetime.now(timezone.utc),
            updated_at=datetime.now(timezone.utc)
        )

        # Act
        active_milestones = project.get_active_milestones()

        # Assert
        assert len(active_milestones) == 2
        assert all(m.state == "open" for m in active_milestones)

    def test_get_completed_milestones(self):
        # Arrange
        milestones = [
            Milestone(1, "M1", None, None, "open", 2, 1),  # Not completed
            Milestone(2, "M2", None, None, "closed", 0, 3),  # Completed (closed)
            Milestone(3, "M3", None, None, "open", 0, 5)  # Completed (100%)
        ]
        project = Project(
            name="Test",
            description="",
            state=ProjectState.ACTIVE,
            milestones=milestones,
            kanban_columns=[],
            created_at=datetime.now(timezone.utc),
            updated_at=datetime.now(timezone.utc)
        )

        # Act
        completed_milestones = project.get_completed_milestones()

        # Assert
        assert len(completed_milestones) == 2

    def test_get_overdue_milestones(self):
        # Arrange
        past_date = datetime.now(timezone.utc) - timedelta(days=1)
        future_date = datetime.now(timezone.utc) + timedelta(days=1)
        milestones = [
            Milestone(1, "M1", None, past_date, "open", 2, 1),  # Overdue
            Milestone(2, "M2", None, future_date, "open", 1, 0),  # Not overdue
            Milestone(3, "M3", None, None, "open", 1, 0)  # No due date
        ]
        project = Project(
            name="Test",
            description="",
            state=ProjectState.ACTIVE,
            milestones=milestones,
            kanban_columns=[],
            created_at=datetime.now(timezone.utc),
            updated_at=datetime.now(timezone.utc)
        )

        # Act
        overdue_milestones = project.get_overdue_milestones()

        # Assert
        assert len(overdue_milestones) == 1
        assert overdue_milestones[0].id == 1

    def test_calculate_overall_progress(self):
        # Arrange
        milestones = [
            Milestone(1, "M1", None, None, "open", 1, 4),  # 80% complete
            Milestone(2, "M2", None, None, "open", 3, 2)   # 40% complete
        ]
        project = Project(
            name="Test",
            description="",
            state=ProjectState.ACTIVE,
            milestones=milestones,
            kanban_columns=[],
            created_at=datetime.now(timezone.utc),
            updated_at=datetime.now(timezone.utc)
        )

        # Act
        progress = project.calculate_overall_progress()

        # Assert
        assert progress == 60.0  # (80 + 40) / 2

    def test_calculate_overall_progress_with_no_milestones(self):
        # Arrange
        project = Project(
            name="Test",
            description="",
            state=ProjectState.ACTIVE,
            milestones=[],
            kanban_columns=[],
            created_at=datetime.now(timezone.utc),
            updated_at=datetime.now(timezone.utc)
        )

        # Act
        progress = project.calculate_overall_progress()

        # Assert
        assert progress == 0.0

    def test_get_total_issues(self):
        # Arrange
        milestones = [
            Milestone(1, "M1", None, None, "open", 2, 3),  # 5 total
            Milestone(2, "M2", None, None, "open", 1, 4)   # 5 total
        ]
        project = Project(
            name="Test",
            description="",
            state=ProjectState.ACTIVE,
            milestones=milestones,
            kanban_columns=[],
            created_at=datetime.now(timezone.utc),
            updated_at=datetime.now(timezone.utc)
        )

        # Act & Assert
        assert project.get_total_issues() == 10
        assert project.get_total_open_issues() == 3
        assert project.get_total_closed_issues() == 7

    def test_archive_project(self):
        # Arrange
        project = Project(
            name="Test",
            description="",
            state=ProjectState.ACTIVE,
            milestones=[],
            kanban_columns=[],
            created_at=datetime.now(timezone.utc),
            updated_at=datetime.now(timezone.utc)
        )

        # Act
        project.archive()

        # Assert
        assert project.state == ProjectState.ARCHIVED
        assert project.archived_at is not None

    def test_activate_project(self):
        # Arrange
        project = Project(
            name="Test",
            description="",
            state=ProjectState.ARCHIVED,
            milestones=[],
            kanban_columns=[],
            created_at=datetime.now(timezone.utc),
            updated_at=datetime.now(timezone.utc),
            archived_at=datetime.now(timezone.utc)
        )

        # Act
        project.activate()

        # Assert
        assert project.state == ProjectState.ACTIVE
        assert project.archived_at is None

    def test_add_milestone(self):
        # Arrange
        project = Project(
            name="Test",
            description="",
            state=ProjectState.ACTIVE,
            milestones=[],
            kanban_columns=[],
            created_at=datetime.now(timezone.utc),
            updated_at=datetime.now(timezone.utc)
        )
        milestone = Milestone(1, "New Milestone", None, None, "open", 0, 0)

        # Act
        project.add_milestone(milestone)

        # Assert
        assert len(project.milestones) == 1
        assert project.milestones[0] == milestone

    def test_add_duplicate_milestone_raises_error(self):
        # Arrange
        milestone1 = Milestone(1, "Milestone 1", None, None, "open", 0, 0)
        milestone2 = Milestone(1, "Milestone 2", None, None, "open", 0, 0)  # Same ID
        project = Project(
            name="Test",
            description="",
            state=ProjectState.ACTIVE,
            milestones=[milestone1],
            kanban_columns=[],
            created_at=datetime.now(timezone.utc),
            updated_at=datetime.now(timezone.utc)
        )

        # Act & Assert
        with pytest.raises(ValueError, match="Milestone with ID 1 already exists"):
            project.add_milestone(milestone2)

    def test_remove_milestone(self):
        # Arrange
        milestone = Milestone(1, "Milestone", None, None, "open", 0, 0)
        project = Project(
            name="Test",
            description="",
            state=ProjectState.ACTIVE,
            milestones=[milestone],
            kanban_columns=[],
            created_at=datetime.now(timezone.utc),
            updated_at=datetime.now(timezone.utc)
        )

        # Act
        project.remove_milestone(1)

        # Assert
        assert len(project.milestones) == 0

    def test_remove_nonexistent_milestone_raises_error(self):
        # Arrange
        project = Project(
            name="Test",
            description="",
            state=ProjectState.ACTIVE,
            milestones=[],
            kanban_columns=[],
            created_at=datetime.now(timezone.utc),
            updated_at=datetime.now(timezone.utc)
        )

        # Act & Assert
        with pytest.raises(ValueError, match="Milestone with ID 999 not found"):
            project.remove_milestone(999)

    def test_get_milestone(self):
        # Arrange
        milestone = Milestone(1, "Milestone", None, None, "open", 0, 0)
        project = Project(
            name="Test",
            description="",
            state=ProjectState.ACTIVE,
            milestones=[milestone],
            kanban_columns=[],
            created_at=datetime.now(timezone.utc),
            updated_at=datetime.now(timezone.utc)
        )

        # Act
        found_milestone = project.get_milestone(1)

        # Assert
        assert found_milestone == milestone

    def test_get_nonexistent_milestone_returns_none(self):
        # Arrange
        project = Project(
            name="Test",
            description="",
            state=ProjectState.ACTIVE,
            milestones=[],
            kanban_columns=[],
            created_at=datetime.now(timezone.utc),
            updated_at=datetime.now(timezone.utc)
        )

        # Act
        found_milestone = project.get_milestone(999)

        # Assert
        assert found_milestone is None