"""Tests for DevAgentManager."""

import os
from unittest.mock import MagicMock, patch, mock_open

import pexpect
import pytest

from app_factory.agents.dev_agent import DevAgentManager, PROMPT_TEMPLATE_PATH


@pytest.fixture
def mock_docker_client():
    return MagicMock()


@pytest.fixture
def manager(mock_docker_client):
    return DevAgentManager(docker_client=mock_docker_client, max_retries=3, timeout=60)


@pytest.fixture
def sample_task():
    return {
        "task_id": "42",
        "title": "Implement login endpoint",
        "description": "Create a POST /login endpoint with JWT",
        "details": "Use bcrypt for password hashing, return JWT token",
        "testStrategy": "Unit test auth flow, integration test endpoint",
    }


class TestInit:
    def test_init_with_client(self, mock_docker_client):
        mgr = DevAgentManager(docker_client=mock_docker_client, max_retries=5, timeout=900)
        assert mgr.docker_client is mock_docker_client
        assert mgr.max_retries == 5
        assert mgr.timeout == 900
        assert mgr._retry_counts == {}

    def test_init_defaults(self, mock_docker_client):
        mgr = DevAgentManager(docker_client=mock_docker_client)
        assert mgr.max_retries == 3
        assert mgr.timeout == 1800

    def test_init_creates_docker_client_from_env(self):
        mock_client = MagicMock()
        mock_docker = MagicMock()
        mock_docker.from_env.return_value = mock_client
        with patch.dict("sys.modules", {"docker": mock_docker}):
            mgr = DevAgentManager()
        assert mgr.docker_client is mock_client


class TestPrepareTaskPrompt:
    def test_includes_all_fields(self, manager, sample_task):
        prompt = manager.prepare_task_prompt(sample_task, global_arch="Microservice arch")
        assert "42" in prompt
        assert "Implement login endpoint" in prompt
        assert "Create a POST /login endpoint with JWT" in prompt
        assert "Use bcrypt for password hashing" in prompt
        assert "Unit test auth flow" in prompt
        assert "Microservice arch" in prompt

    def test_without_global_arch(self, manager, sample_task):
        prompt = manager.prepare_task_prompt(sample_task)
        assert "No architecture context provided." in prompt
        assert "42" in prompt
        assert "Implement login endpoint" in prompt

    def test_with_empty_global_arch(self, manager, sample_task):
        prompt = manager.prepare_task_prompt(sample_task, global_arch="")
        assert "No architecture context provided." in prompt

    def test_uses_id_fallback(self, manager):
        task = {"id": "99", "title": "Fallback task", "description": "desc"}
        prompt = manager.prepare_task_prompt(task)
        assert "99" in prompt

    def test_template_file_exists(self):
        assert PROMPT_TEMPLATE_PATH.exists(), f"Template not found at {PROMPT_TEMPLATE_PATH}"


@pytest.mark.asyncio
class TestExecuteTask:
    async def test_success_path(self, manager, sample_task, tmp_path):
        worktree = str(tmp_path)
        mock_child = MagicMock()
        mock_child.before = "Created src/auth.py\nModified tests/test_auth.py\n2 passed"
        mock_child.exitstatus = 0

        with patch("app_factory.agents.dev_agent.pexpect.spawn", return_value=mock_child):
            result = await manager.execute_task(sample_task, "container123", worktree)

        assert result["status"] == "success"
        assert result["exit_code"] == 0
        assert "src/auth.py" in result["files_changed"]
        assert isinstance(result["output"], str)

    async def test_failure_path(self, manager, sample_task, tmp_path):
        worktree = str(tmp_path)
        mock_child = MagicMock()
        mock_child.before = "Error: compilation failed\n1 failed"
        mock_child.exitstatus = 1

        with patch("app_factory.agents.dev_agent.pexpect.spawn", return_value=mock_child):
            result = await manager.execute_task(sample_task, "container123", worktree)

        assert result["status"] == "failed"
        assert result["exit_code"] == 1

    async def test_timeout(self, manager, sample_task, tmp_path):
        worktree = str(tmp_path)
        mock_child = MagicMock()
        mock_child.expect.side_effect = pexpect.TIMEOUT("timed out")
        mock_child.close.return_value = None

        with patch("app_factory.agents.dev_agent.pexpect.spawn", return_value=mock_child):
            result = await manager.execute_task(sample_task, "container123", worktree)

        assert result["status"] == "failed"
        assert result["output"] == "timeout"
        assert result["exit_code"] == -1
        assert result["files_changed"] == []

    async def test_writes_prompt_file(self, manager, sample_task, tmp_path):
        worktree = str(tmp_path)
        mock_child = MagicMock()
        mock_child.before = "done"
        mock_child.exitstatus = 0

        written_content = None

        original_open = open

        def capturing_open(path, *args, **kwargs):
            nonlocal written_content
            if str(path).endswith(".task_prompt.txt") and "w" in (args[0] if args else ""):
                result = original_open(path, *args, **kwargs)
                # We'll check the file exists during execution
                return result
            return original_open(path, *args, **kwargs)

        with patch("app_factory.agents.dev_agent.pexpect.spawn", return_value=mock_child):
            await manager.execute_task(sample_task, "cid", worktree)

        # Prompt file should be cleaned up after execution
        prompt_path = os.path.join(worktree, ".task_prompt.txt")
        assert not os.path.exists(prompt_path), "Prompt file should be cleaned up"

    async def test_spawns_correct_command(self, manager, sample_task, tmp_path):
        worktree = str(tmp_path)
        mock_child = MagicMock()
        mock_child.before = ""
        mock_child.exitstatus = 0

        with patch("app_factory.agents.dev_agent.pexpect.spawn", return_value=mock_child) as mock_spawn:
            await manager.execute_task(sample_task, "abc123", worktree)

        mock_spawn.assert_called_once_with(
            "docker exec abc123 claude --print --prompt-file /workspace/.task_prompt.txt",
            timeout=60,
            encoding="utf-8",
        )

    async def test_none_exitstatus_treated_as_error(self, manager, sample_task, tmp_path):
        worktree = str(tmp_path)
        mock_child = MagicMock()
        mock_child.before = "output"
        mock_child.exitstatus = None

        with patch("app_factory.agents.dev_agent.pexpect.spawn", return_value=mock_child):
            result = await manager.execute_task(sample_task, "cid", worktree)

        assert result["status"] == "failed"
        assert result["exit_code"] == -1


class TestParseClaudeOutput:
    def test_extracts_files_changed(self, manager):
        output = "Created src/auth.py\nModified tests/test_auth.py\nUpdated config.json"
        result = manager.parse_claude_output(output)
        assert "src/auth.py" in result["files_changed"]
        assert "tests/test_auth.py" in result["files_changed"]
        assert "config.json" in result["files_changed"]

    def test_extracts_test_results(self, manager):
        output = "Results: 5 passed, 2 failed"
        result = manager.parse_claude_output(output)
        assert result["test_results"]["passed"] == 5
        assert result["test_results"]["failed"] == 2

    def test_extracts_errors(self, manager):
        output = "Error: could not import module\nFAILED: assertion mismatch"
        result = manager.parse_claude_output(output)
        assert len(result["errors"]) >= 1

    def test_empty_output(self, manager):
        result = manager.parse_claude_output("")
        assert result["files_changed"] == []
        assert result["test_results"] == {}
        assert result["errors"] == []

    def test_no_test_results(self, manager):
        output = "Created app.py\nDone."
        result = manager.parse_claude_output(output)
        assert result["test_results"] == {}

    def test_deduplicates_files(self, manager):
        output = "Created app.py\nEditing app.py"
        result = manager.parse_claude_output(output)
        assert result["files_changed"].count("app.py") == 1


@pytest.mark.asyncio
class TestExecuteWithRetry:
    async def test_succeeds_on_first_try(self, manager, sample_task, tmp_path):
        worktree = str(tmp_path)
        mock_child = MagicMock()
        mock_child.before = "All good\n3 passed"
        mock_child.exitstatus = 0

        with patch("app_factory.agents.dev_agent.pexpect.spawn", return_value=mock_child):
            result = await manager.execute_with_retry(sample_task, "cid", worktree)

        assert result["status"] == "success"
        assert manager.get_retry_count("42") == 1

    async def test_succeeds_after_failures(self, manager, sample_task, tmp_path):
        worktree = str(tmp_path)

        # First two calls fail, third succeeds
        fail_child = MagicMock()
        fail_child.before = "Error: build failed"
        fail_child.exitstatus = 1

        success_child = MagicMock()
        success_child.before = "All good"
        success_child.exitstatus = 0

        with patch(
            "app_factory.agents.dev_agent.pexpect.spawn",
            side_effect=[fail_child, fail_child, success_child],
        ):
            result = await manager.execute_with_retry(sample_task, "cid", worktree)

        assert result["status"] == "success"
        assert manager.get_retry_count("42") == 3

    async def test_max_retries_exceeded(self, manager, sample_task, tmp_path):
        worktree = str(tmp_path)
        mock_child = MagicMock()
        mock_child.before = "Error: persistent failure"
        mock_child.exitstatus = 1

        with patch("app_factory.agents.dev_agent.pexpect.spawn", return_value=mock_child):
            result = await manager.execute_with_retry(sample_task, "cid", worktree)

        assert result["status"] == "needs_clarification"
        assert manager.get_retry_count("42") == 3


class TestRetryCounters:
    def test_get_retry_count_default(self, manager):
        assert manager.get_retry_count("unknown") == 0

    def test_get_retry_count_after_set(self, manager):
        manager._retry_counts["task-1"] = 2
        assert manager.get_retry_count("task-1") == 2

    def test_reset_retry_count(self, manager):
        manager._retry_counts["task-1"] = 3
        manager.reset_retry_count("task-1")
        assert manager.get_retry_count("task-1") == 0

    def test_reset_nonexistent_task(self, manager):
        # Should not raise
        manager.reset_retry_count("nonexistent")
        assert manager.get_retry_count("nonexistent") == 0
