"""Tests for the Tickets & Sprints task management system.

Covers:
  - Ticket CRUD operations
  - Sprint CRUD operations
  - Tool assignment (vscode, aider, claude-code, etc.)
  - Filtering by status, priority, tool, tag
  - Kanban board view
  - Sprint statistics
  - Comment system
  - Multi-tool workflow integration
"""

from __future__ import annotations

import pytest
from pathlib import Path

from proxym.dashboard.tickets import (
    Milestone,
    MilestoneStatus,
    Sprint,
    SprintStatus,
    Ticket,
    TicketComment,
    TicketManager,
    TicketPriority,
    TicketStatus,
    TicketType,
    ToolAssignment,
)


@pytest.fixture
def mgr(tmp_path):
    """TicketManager with temp persistence."""
    return TicketManager(persist_path=tmp_path / "tickets.json")


@pytest.fixture
def mgr_no_persist():
    """TicketManager without persistence."""
    return TicketManager()


@pytest.fixture
def sample_ticket():
    return Ticket(
        title="Fix login bug",
        description="Users cannot login with OAuth",
        priority=TicketPriority.HIGH,
        type=TicketType.BUG,
        tags=["auth", "urgent"],
        files=["src/auth/login.py", "src/auth/oauth.py"],
        estimated_hours=2.0,
    )


@pytest.fixture
def sample_sprint():
    return Sprint(
        name="Sprint 1 - Auth fixes",
        goal="Fix all authentication issues",
        start_date="2026-03-23",
        end_date="2026-04-06",
    )


@pytest.fixture
def sample_milestone():
    return Milestone(
        name="Milestone 1 - Release",
        goal="Ship the auth improvements",
        start_date="2026-03-23",
        end_date="2026-04-30",
    )


# ── Ticket Model Tests ───────────────────────────────────────

class TestTicketModel:
    def test_ticket_has_auto_id(self):
        t = Ticket(title="Test")
        assert t.id.startswith("TKT-")
        assert len(t.id) == 10  # TKT- + 6 hex chars

    def test_ticket_defaults(self):
        t = Ticket(title="Test")
        assert t.status == TicketStatus.TODO
        assert t.priority == TicketPriority.MEDIUM
        assert t.type == TicketType.TASK
        assert t.sprint_id == ""
        assert t.assigned_tool is None
        assert t.tags == []
        assert t.files == []
        assert t.created_at > 0

    def test_ticket_summary(self, sample_ticket):
        s = sample_ticket.summary()
        assert s["title"] == "Fix login bug"
        assert s["priority"] == "high"
        assert s["type"] == "bug"
        assert s["tags"] == ["auth", "urgent"]
        assert s["files"] == ["src/auth/login.py", "src/auth/oauth.py"]
        assert s["estimated_hours"] == 2.0
        assert s["assigned_tool"] is None

    def test_ticket_summary_with_tool(self, sample_ticket):
        sample_ticket.assigned_tool = ToolAssignment(tool="aider", agent_mode="code")
        s = sample_ticket.summary()
        assert s["assigned_tool"]["tool"] == "aider"
        assert s["assigned_tool"]["agent_mode"] == "code"


class TestSprintModel:
    def test_sprint_has_auto_id(self):
        s = Sprint(name="Test Sprint")
        assert s.id.startswith("SPR-")

    def test_sprint_summary_empty(self):
        s = Sprint(name="Test Sprint", goal="Do things")
        summary = s.summary([])
        assert summary["name"] == "Test Sprint"
        assert summary["tickets_total"] == 0
        assert summary["progress_pct"] == 0

    def test_sprint_summary_with_tickets(self):
        s = Sprint(name="Test Sprint")
        tickets = [
            Ticket(title="A", status=TicketStatus.DONE),
            Ticket(title="B", status=TicketStatus.IN_PROGRESS),
            Ticket(title="C", status=TicketStatus.TODO),
        ]
        summary = s.summary(tickets)
        assert summary["tickets_total"] == 3
        assert summary["tickets_done"] == 1
        assert summary["tickets_in_progress"] == 1
        assert summary["progress_pct"] == pytest.approx(33.3, abs=0.1)


class TestMilestoneModel:
    def test_milestone_has_auto_id(self):
        milestone = Milestone(name="Release 1")
        assert milestone.id.startswith("MLS-")

    def test_milestone_summary_empty(self):
        milestone = Milestone(name="Release 1", goal="Ship auth fixes")
        summary = milestone.summary([])
        assert summary["name"] == "Release 1"
        assert summary["sprints_total"] == 0
        assert summary["tickets_total"] == 0
        assert summary["progress_pct"] == 0

    def test_milestone_summary_with_groups(self):
        milestone = Milestone(name="Release 1")
        sprints = [
            Sprint(name="Sprint A", milestone_id=milestone.id, status=SprintStatus.COMPLETED),
            Sprint(name="Sprint B", milestone_id=milestone.id, status=SprintStatus.ACTIVE),
        ]
        tickets = [
            Ticket(title="A", status=TicketStatus.DONE),
            Ticket(title="B", status=TicketStatus.IN_PROGRESS),
            Ticket(title="C", status=TicketStatus.TODO),
        ]
        summary = milestone.summary(sprints, tickets)
        assert summary["sprints_total"] == 2
        assert summary["sprints_completed"] == 1
        assert summary["sprints_in_progress"] == 1
        assert summary["tickets_total"] == 3
        assert summary["tickets_done"] == 1
        assert summary["tickets_in_progress"] == 1


# ── TicketManager CRUD Tests ─────────────────────────────────

class TestTicketCRUD:
    def test_create_ticket(self, mgr, sample_ticket):
        created = mgr.create_ticket(sample_ticket)
        assert created.id == sample_ticket.id
        assert mgr.get_ticket(created.id) is not None

    def test_list_tickets_empty(self, mgr):
        assert mgr.list_tickets() == []

    def test_list_tickets(self, mgr):
        mgr.create_ticket(Ticket(title="A"))
        mgr.create_ticket(Ticket(title="B"))
        assert len(mgr.list_tickets()) == 2

    def test_get_ticket(self, mgr, sample_ticket):
        mgr.create_ticket(sample_ticket)
        t = mgr.get_ticket(sample_ticket.id)
        assert t.title == "Fix login bug"

    def test_get_ticket_not_found(self, mgr):
        assert mgr.get_ticket("nonexistent") is None

    def test_update_ticket(self, mgr, sample_ticket):
        mgr.create_ticket(sample_ticket)
        updated = mgr.update_ticket(sample_ticket.id, {"title": "Updated title", "status": "in_progress"})
        assert updated.title == "Updated title"
        assert updated.status == TicketStatus.IN_PROGRESS

    def test_update_ticket_not_found(self, mgr):
        assert mgr.update_ticket("nonexistent", {"title": "X"}) is None

    def test_update_ticket_to_done_sets_completed_at(self, mgr, sample_ticket):
        mgr.create_ticket(sample_ticket)
        updated = mgr.update_ticket(sample_ticket.id, {"status": "done"})
        assert updated.completed_at > 0

    def test_delete_ticket(self, mgr, sample_ticket):
        mgr.create_ticket(sample_ticket)
        assert mgr.delete_ticket(sample_ticket.id) is True
        assert mgr.get_ticket(sample_ticket.id) is None

    def test_delete_ticket_not_found(self, mgr):
        assert mgr.delete_ticket("nonexistent") is False


# ── Filtering Tests ───────────────────────────────────────────

class TestTicketFiltering:
    def test_filter_by_status(self, mgr):
        mgr.create_ticket(Ticket(title="A", status=TicketStatus.TODO))
        mgr.create_ticket(Ticket(title="B", status=TicketStatus.DONE))
        mgr.create_ticket(Ticket(title="C", status=TicketStatus.TODO))
        result = mgr.list_tickets(status=TicketStatus.TODO)
        assert len(result) == 2
        assert all(t.status == TicketStatus.TODO for t in result)

    def test_filter_by_priority(self, mgr):
        mgr.create_ticket(Ticket(title="A", priority=TicketPriority.HIGH))
        mgr.create_ticket(Ticket(title="B", priority=TicketPriority.LOW))
        result = mgr.list_tickets(priority=TicketPriority.HIGH)
        assert len(result) == 1
        assert result[0].title == "A"

    def test_filter_by_tag(self, mgr):
        mgr.create_ticket(Ticket(title="A", tags=["auth"]))
        mgr.create_ticket(Ticket(title="B", tags=["ui"]))
        mgr.create_ticket(Ticket(title="C", tags=["auth", "ui"]))
        result = mgr.list_tickets(tag="auth")
        assert len(result) == 2

    def test_filter_by_tool(self, mgr):
        t1 = Ticket(title="A")
        t1.assigned_tool = ToolAssignment(tool="aider")
        t2 = Ticket(title="B")
        t2.assigned_tool = ToolAssignment(tool="vscode")
        t3 = Ticket(title="C")  # unassigned
        mgr.create_ticket(t1)
        mgr.create_ticket(t2)
        mgr.create_ticket(t3)
        result = mgr.list_tickets(tool="aider")
        assert len(result) == 1
        assert result[0].title == "A"

    def test_filter_by_sprint(self, mgr):
        mgr.create_ticket(Ticket(title="A", sprint_id="SPR-001"))
        mgr.create_ticket(Ticket(title="B", sprint_id="SPR-002"))
        mgr.create_ticket(Ticket(title="C", sprint_id="SPR-001"))
        result = mgr.list_tickets(sprint_id="SPR-001")
        assert len(result) == 2


# ── Tool Assignment Tests ─────────────────────────────────────

class TestToolAssignment:
    def test_assign_tool(self, mgr, sample_ticket):
        mgr.create_ticket(sample_ticket)
        assignment = ToolAssignment(tool="aider", agent_mode="code", model="claude-sonnet-4-6")
        result = mgr.assign_tool(sample_ticket.id, assignment)
        assert result is True
        ticket = mgr.get_ticket(sample_ticket.id)
        assert ticket.assigned_tool.tool == "aider"
        assert ticket.assigned_tool.agent_mode == "code"
        assert ticket.assigned_tool.model == "claude-sonnet-4-6"
        assert ticket.assigned_tool.started_at > 0

    def test_assign_tool_moves_to_in_progress(self, mgr):
        ticket = Ticket(title="Test", status=TicketStatus.TODO)
        mgr.create_ticket(ticket)
        mgr.assign_tool(ticket.id, ToolAssignment(tool="vscode"))
        updated = mgr.get_ticket(ticket.id)
        assert updated.status == TicketStatus.IN_PROGRESS

    def test_assign_tool_not_found(self, mgr):
        assert mgr.assign_tool("fake", ToolAssignment(tool="aider")) is False

    def test_reassign_tool(self, mgr, sample_ticket):
        mgr.create_ticket(sample_ticket)
        mgr.assign_tool(sample_ticket.id, ToolAssignment(tool="vscode"))
        mgr.assign_tool(sample_ticket.id, ToolAssignment(tool="aider"))
        ticket = mgr.get_ticket(sample_ticket.id)
        assert ticket.assigned_tool.tool == "aider"


# ── Comment Tests ─────────────────────────────────────────────

class TestComments:
    def test_add_comment(self, mgr, sample_ticket):
        mgr.create_ticket(sample_ticket)
        comment = TicketComment(author="aider", content="Fixed the OAuth redirect issue")
        result = mgr.add_comment(sample_ticket.id, comment)
        assert result is True
        ticket = mgr.get_ticket(sample_ticket.id)
        assert len(ticket.comments) == 1
        assert ticket.comments[0].author == "aider"
        assert ticket.comments[0].content == "Fixed the OAuth redirect issue"

    def test_add_multiple_comments(self, mgr, sample_ticket):
        mgr.create_ticket(sample_ticket)
        mgr.add_comment(sample_ticket.id, TicketComment(author="vscode", content="Started analysis"))
        mgr.add_comment(sample_ticket.id, TicketComment(author="aider", content="Proposed fix"))
        mgr.add_comment(sample_ticket.id, TicketComment(author="claude-code", content="Review complete"))
        ticket = mgr.get_ticket(sample_ticket.id)
        assert len(ticket.comments) == 3

    def test_add_comment_not_found(self, mgr):
        assert mgr.add_comment("fake", TicketComment(content="test")) is False


# ── Sprint CRUD Tests ─────────────────────────────────────────

class TestSprintCRUD:
    def test_create_sprint(self, mgr, sample_sprint):
        created = mgr.create_sprint(sample_sprint)
        assert created.id.startswith("SPR-")
        assert mgr.get_sprint(created.id) is not None

    def test_list_sprints(self, mgr):
        mgr.create_sprint(Sprint(name="S1"))
        mgr.create_sprint(Sprint(name="S2"))
        assert len(mgr.list_sprints()) == 2

    def test_filter_sprints_by_status(self, mgr):
        mgr.create_sprint(Sprint(name="S1", status=SprintStatus.ACTIVE))
        mgr.create_sprint(Sprint(name="S2", status=SprintStatus.COMPLETED))
        result = mgr.list_sprints(status=SprintStatus.ACTIVE)
        assert len(result) == 1
        assert result[0].name == "S1"

    def test_update_sprint(self, mgr, sample_sprint):
        mgr.create_sprint(sample_sprint)
        updated = mgr.update_sprint(sample_sprint.id, {"status": "active", "goal": "New goal"})
        assert updated.status == SprintStatus.ACTIVE
        assert updated.goal == "New goal"

    def test_update_sprint_not_found(self, mgr):
        assert mgr.update_sprint("fake", {"name": "X"}) is None

    def test_delete_sprint(self, mgr, sample_sprint):
        mgr.create_sprint(sample_sprint)
        assert mgr.delete_sprint(sample_sprint.id) is True
        assert mgr.get_sprint(sample_sprint.id) is None

    def test_delete_sprint_not_found(self, mgr):
        assert mgr.delete_sprint("fake") is False


class TestMilestoneCRUD:
    def test_create_milestone(self, mgr, sample_milestone):
        created = mgr.create_milestone(sample_milestone)
        assert created.id == sample_milestone.id
        assert mgr.get_milestone(created.id) is not None

    def test_list_milestones(self, mgr):
        mgr.create_milestone(Milestone(name="M1"))
        mgr.create_milestone(Milestone(name="M2"))
        assert len(mgr.list_milestones()) == 2

    def test_update_milestone(self, mgr, sample_milestone):
        mgr.create_milestone(sample_milestone)
        updated = mgr.update_milestone(sample_milestone.id, {"status": "active", "goal": "New goal"})
        assert updated.status == MilestoneStatus.ACTIVE
        assert updated.goal == "New goal"

    def test_delete_milestone(self, mgr, sample_milestone):
        mgr.create_milestone(sample_milestone)
        assert mgr.delete_milestone(sample_milestone.id) is True
        assert mgr.get_milestone(sample_milestone.id) is None

    def test_delete_milestone_not_found(self, mgr):
        assert mgr.delete_milestone("fake") is False

    def test_list_sprints_by_milestone(self, mgr, sample_milestone):
        mgr.create_milestone(sample_milestone)
        sprint_a = Sprint(name="S1", milestone_id=sample_milestone.id)
        sprint_b = Sprint(name="S2", milestone_id="other")
        mgr.create_sprint(sprint_a)
        mgr.create_sprint(sprint_b)
        result = mgr.list_sprints(milestone_id=sample_milestone.id)
        assert len(result) == 1
        assert result[0].id == sprint_a.id

    def test_delete_sprint_clears_ticket_links(self, mgr):
        sprint = Sprint(name="S1")
        mgr.create_sprint(sprint)
        ticket = Ticket(title="Linked ticket", sprint_id=sprint.id)
        mgr.create_ticket(ticket)

        assert mgr.delete_sprint(sprint.id) is True
        assert mgr.get_ticket(ticket.id).sprint_id == ""

    def test_delete_milestone_clears_sprint_links(self, mgr, sample_milestone):
        mgr.create_milestone(sample_milestone)
        sprint = Sprint(name="S1", milestone_id=sample_milestone.id)
        mgr.create_sprint(sprint)

        assert mgr.delete_milestone(sample_milestone.id) is True
        assert mgr.get_sprint(sprint.id).milestone_id == ""


# ── Sprint Stats Tests ────────────────────────────────────────

class TestSprintStats:
    def test_sprint_tickets(self, mgr, sample_sprint):
        mgr.create_sprint(sample_sprint)
        sid = sample_sprint.id
        mgr.create_ticket(Ticket(title="A", sprint_id=sid))
        mgr.create_ticket(Ticket(title="B", sprint_id=sid))
        mgr.create_ticket(Ticket(title="C", sprint_id="other"))
        assert len(mgr.sprint_tickets(sid)) == 2

    def test_sprint_stats(self, mgr, sample_sprint):
        mgr.create_sprint(sample_sprint)
        sid = sample_sprint.id
        t1 = Ticket(title="A", sprint_id=sid, status=TicketStatus.DONE, estimated_hours=2, actual_hours=1.5)
        t1.assigned_tool = ToolAssignment(tool="aider")
        t2 = Ticket(title="B", sprint_id=sid, status=TicketStatus.IN_PROGRESS, estimated_hours=3)
        t2.assigned_tool = ToolAssignment(tool="vscode")
        t3 = Ticket(title="C", sprint_id=sid, status=TicketStatus.TODO, estimated_hours=1)
        mgr.create_ticket(t1)
        mgr.create_ticket(t2)
        mgr.create_ticket(t3)

        stats = mgr.sprint_stats(sid)
        assert stats["total_tickets"] == 3
        assert stats["by_status"]["done"] == 1
        assert stats["by_status"]["in_progress"] == 1
        assert stats["by_status"]["todo"] == 1
        assert stats["by_tool"]["aider"] == 1
        assert stats["by_tool"]["vscode"] == 1
        assert stats["total_estimated_hours"] == 6.0
        assert stats["total_actual_hours"] == 1.5


# ── Board View Tests ──────────────────────────────────────────

class TestBoardView:
    def test_board_empty(self, mgr):
        board = mgr.board()
        assert all(v == [] for v in board.values())
        assert "todo" in board
        assert "done" in board

    def test_board_with_tickets(self, mgr):
        mgr.create_ticket(Ticket(title="A", status=TicketStatus.TODO))
        mgr.create_ticket(Ticket(title="B", status=TicketStatus.IN_PROGRESS))
        mgr.create_ticket(Ticket(title="C", status=TicketStatus.DONE))
        board = mgr.board()
        assert len(board["todo"]) == 1
        assert len(board["in_progress"]) == 1
        assert len(board["done"]) == 1

    def test_board_filtered_by_sprint(self, mgr):
        mgr.create_ticket(Ticket(title="A", status=TicketStatus.TODO, sprint_id="S1"))
        mgr.create_ticket(Ticket(title="B", status=TicketStatus.TODO, sprint_id="S2"))
        board = mgr.board(sprint_id="S1")
        assert len(board["todo"]) == 1
        assert board["todo"][0]["title"] == "A"


# ── Persistence Tests ─────────────────────────────────────────

class TestPersistence:
    def test_save_and_load(self, tmp_path):
        path = tmp_path / "tickets.json"
        mgr1 = TicketManager(persist_path=path)
        mgr1.create_ticket(Ticket(title="Persisted ticket", tags=["test"]))
        milestone = mgr1.create_milestone(Milestone(name="Persisted milestone"))
        mgr1.create_sprint(Sprint(name="Persisted sprint", milestone_id=milestone.id))

        mgr2 = TicketManager(persist_path=path)
        assert len(mgr2.list_tickets()) == 1
        assert mgr2.list_tickets()[0].title == "Persisted ticket"
        assert len(mgr2.list_sprints()) == 1
        assert mgr2.list_sprints()[0].milestone_id == milestone.id
        assert len(mgr2.list_milestones()) == 1

    def test_no_persist_path(self, mgr_no_persist):
        mgr_no_persist.create_ticket(Ticket(title="Test"))
        assert len(mgr_no_persist.list_tickets()) == 1

    def test_load_from_corrupted_file(self, tmp_path):
        path = tmp_path / "tickets.json"
        path.write_text("{invalid json")
        mgr = TicketManager(persist_path=path)
        assert len(mgr.list_tickets()) == 0


# ── Multi-Tool Workflow Tests ─────────────────────────────────

class TestMultiToolWorkflow:
    """Test realistic multi-tool workflows — the core use case."""

    def test_vscode_creates_aider_executes(self, mgr):
        """Simulate: user creates ticket in dashboard, aider picks it up."""
        ticket = Ticket(
            title="Refactor auth module",
            type=TicketType.REFACTOR,
            files=["src/auth/login.py", "src/auth/oauth.py"],
            tags=["refactor"],
        )
        mgr.create_ticket(ticket)

        mgr.assign_tool(ticket.id, ToolAssignment(tool="aider", agent_mode="code", model="claude-sonnet-4-6"))
        mgr.add_comment(ticket.id, TicketComment(author="aider", content="Starting refactor of auth module"))
        mgr.update_ticket(ticket.id, {"status": "in_review"})
        mgr.add_comment(ticket.id, TicketComment(author="aider", content="Refactor complete, 2 files changed"))
        mgr.update_ticket(ticket.id, {"status": "done", "actual_hours": 0.5})

        t = mgr.get_ticket(ticket.id)
        assert t.status == TicketStatus.DONE
        assert t.assigned_tool.tool == "aider"
        assert len(t.comments) == 2
        assert t.actual_hours == 0.5
        assert t.completed_at > 0

    def test_claude_code_and_windsurf_parallel(self, mgr):
        """Two tools working on different tickets in the same sprint."""
        sprint = Sprint(name="Sprint 2", status=SprintStatus.ACTIVE)
        mgr.create_sprint(sprint)

        t1 = Ticket(title="Add tests", type=TicketType.TEST, sprint_id=sprint.id)
        t2 = Ticket(title="Fix CSS", type=TicketType.BUG, sprint_id=sprint.id)
        mgr.create_ticket(t1)
        mgr.create_ticket(t2)

        mgr.assign_tool(t1.id, ToolAssignment(tool="claude-code", model="claude-opus-4-6"))
        mgr.assign_tool(t2.id, ToolAssignment(tool="windsurf", agent_mode="code"))

        stats = mgr.sprint_stats(sprint.id)
        assert stats["total_tickets"] == 2
        assert stats["by_tool"]["claude-code"] == 1
        assert stats["by_tool"]["windsurf"] == 1

    def test_full_sprint_lifecycle(self, mgr):
        """Sprint: planning → active → all tickets done → completed."""
        sprint = Sprint(name="Sprint 3")
        mgr.create_sprint(sprint)

        tickets = []
        for title, tool in [("Task A", "vscode"), ("Task B", "aider"), ("Task C", "claude-code")]:
            t = Ticket(title=title, sprint_id=sprint.id)
            mgr.create_ticket(t)
            mgr.assign_tool(t.id, ToolAssignment(tool=tool))
            tickets.append(t)

        mgr.update_sprint(sprint.id, {"status": "active"})

        for t in tickets:
            mgr.update_ticket(t.id, {"status": "done"})

        mgr.update_sprint(sprint.id, {"status": "completed"})

        s = mgr.get_sprint(sprint.id)
        assert s.status == SprintStatus.COMPLETED
        sprint_summary = s.summary(mgr.sprint_tickets(sprint.id))
        assert sprint_summary["progress_pct"] == 100.0
        assert sprint_summary["tickets_done"] == 3

    def test_cursor_and_roo_code_handoff(self, mgr):
        """Ticket started by cursor, handed off to roo-code."""
        ticket = Ticket(title="Implement feature X", type=TicketType.FEATURE)
        mgr.create_ticket(ticket)

        mgr.assign_tool(ticket.id, ToolAssignment(tool="cursor", agent_mode="architect"))
        mgr.add_comment(ticket.id, TicketComment(author="cursor", content="Architecture designed"))

        mgr.assign_tool(ticket.id, ToolAssignment(tool="roo-code", agent_mode="code"))
        mgr.add_comment(ticket.id, TicketComment(author="roo-code", content="Implementation complete"))

        t = mgr.get_ticket(ticket.id)
        assert t.assigned_tool.tool == "roo-code"
        assert len(t.comments) == 2
