"""Tests for QAAgent."""

import os
import subprocess
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

import git as gitmod
import pytest

from app_factory.agents.qa_agent import QAAgent


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_review_response(text, input_tokens=10, output_tokens=20):
    """Build a fake Claude SDK completion response."""
    return SimpleNamespace(text=text, input_tokens=input_tokens, output_tokens=output_tokens)


def _build_agent(repo_path="/fake/repo", **kwargs):
    """Create a QAAgent with mocked git.Repo and Claude SDK client."""
    with patch("app_factory.agents.qa_agent.git.Repo") as mock_repo_cls, \
         patch("app_factory.agents.qa_agent.ClaudeSDKClient") as mock_sdk_client:
        mock_repo = MagicMock()
        mock_repo_cls.return_value = mock_repo
        mock_client = AsyncMock()
        mock_sdk_client.return_value = mock_client
        agent = QAAgent(repo_path=repo_path, api_key="test-key", **kwargs)
    agent.client = mock_client
    agent.repo = mock_repo
    return agent, mock_repo, mock_client


APPROVED_REVIEW = """\
APPROVED: true
ISSUES:
- [severity: info] Minor style suggestion
SUMMARY: Code looks good overall."""

REJECTED_REVIEW = """\
APPROVED: false
ISSUES:
- [severity: critical] SQL injection in query builder
- [severity: warning] Missing input validation
SUMMARY: Critical security issue found."""


# ---------------------------------------------------------------------------
# Initialization
# ---------------------------------------------------------------------------

class TestInitialization:
    def test_no_api_key_uses_default_client(self):
        with patch("app_factory.agents.qa_agent.git.Repo"), \
             patch("app_factory.agents.qa_agent.ClaudeSDKClient") as mock_sdk_client, \
             patch.dict(os.environ, {}, clear=True):
            env = os.environ.copy()
            env.pop("ANTHROPIC_API_KEY", None)
            with patch.dict(os.environ, env, clear=True):
                mock_sdk_client.return_value = AsyncMock()
                agent = QAAgent(repo_path="/fake")
                mock_sdk_client.assert_called_once_with(
                    api_key=None,
                    auth_token=None,
                    enable_debug=False,
                )

    def test_creates_with_api_key(self):
        agent, mock_repo, _ = _build_agent()
        assert agent.max_retries == 3

    def test_custom_max_retries(self):
        agent, _, _ = _build_agent(max_retries=5)
        assert agent.max_retries == 5


# ---------------------------------------------------------------------------
# Rebase
# ---------------------------------------------------------------------------

class TestRebaseOntoMain:
    @pytest.mark.asyncio
    async def test_rebase_success(self):
        agent, _, _ = _build_agent()
        mock_wt_repo = MagicMock()
        with patch("app_factory.agents.qa_agent.git.Repo", return_value=mock_wt_repo):
            result = await agent.rebase_onto_main("/worktree/path", "task-1")

        assert result["success"] is True
        assert result["conflicts"] == []
        mock_wt_repo.git.rebase.assert_called_once_with("main")

    @pytest.mark.asyncio
    async def test_rebase_conflict_unresolvable(self):
        agent, _, _ = _build_agent()
        mock_wt_repo = MagicMock()
        mock_wt_repo.git.rebase.side_effect = gitmod.GitCommandError("rebase", "CONFLICT")
        mock_wt_repo.git.status.return_value = "UU conflicted_file.py"

        with patch("app_factory.agents.qa_agent.git.Repo", return_value=mock_wt_repo), \
             patch.object(agent, "auto_resolve_conflicts", return_value=False):
            result = await agent.rebase_onto_main("/worktree/path", "task-1")

        assert result["success"] is False
        assert "conflicted_file.py" in result["conflicts"]

    @pytest.mark.asyncio
    async def test_rebase_conflict_auto_resolved(self):
        agent, _, _ = _build_agent()
        mock_wt_repo = MagicMock()
        mock_wt_repo.git.rebase.side_effect = gitmod.GitCommandError("rebase", "CONFLICT")
        mock_wt_repo.git.status.return_value = "UU file.py"

        with patch("app_factory.agents.qa_agent.git.Repo", return_value=mock_wt_repo), \
             patch.object(agent, "auto_resolve_conflicts", return_value=True):
            result = await agent.rebase_onto_main("/worktree/path", "task-1")

        assert result["success"] is True

    @pytest.mark.asyncio
    async def test_fetch_failure_continues(self):
        """If fetch fails (no remote), rebase should still be attempted."""
        agent, _, _ = _build_agent()
        mock_wt_repo = MagicMock()
        mock_wt_repo.git.fetch.side_effect = gitmod.GitCommandError("fetch", "No remote")

        with patch("app_factory.agents.qa_agent.git.Repo", return_value=mock_wt_repo):
            result = await agent.rebase_onto_main("/worktree/path", "task-1")

        assert result["success"] is True
        mock_wt_repo.git.rebase.assert_called_once_with("main")


# ---------------------------------------------------------------------------
# Linter
# ---------------------------------------------------------------------------

class TestRunLinter:
    def test_lint_passes(self):
        agent, _, _ = _build_agent()
        mock_result = subprocess.CompletedProcess(
            args=["ruff", "check", "."],
            returncode=0,
            stdout="All checks passed!\n",
            stderr="",
        )
        with patch("app_factory.agents.qa_agent.subprocess.run", return_value=mock_result):
            result = agent.run_linter("/worktree/path")

        assert result["passed"] is True
        assert result["errors"] == []

    def test_lint_fails_with_errors(self):
        agent, _, _ = _build_agent()
        ruff_output = (
            "app/main.py:10:1: E501 Line too long (120 > 88 characters)\n"
            "app/main.py:15:5: F841 Local variable 'x' is assigned but never used\n"
            "Found 2 errors.\n"
        )
        mock_result = subprocess.CompletedProcess(
            args=["ruff", "check", "."],
            returncode=1,
            stdout=ruff_output,
            stderr="",
        )
        with patch("app_factory.agents.qa_agent.subprocess.run", return_value=mock_result):
            result = agent.run_linter("/worktree/path")

        assert result["passed"] is False
        assert len(result["errors"]) == 2

    def test_lint_ruff_not_found(self):
        agent, _, _ = _build_agent()
        with patch("app_factory.agents.qa_agent.subprocess.run", side_effect=FileNotFoundError):
            result = agent.run_linter("/worktree/path")

        assert result["passed"] is True
        assert "ruff not found" in result["warnings"][0]

    def test_lint_timeout(self):
        agent, _, _ = _build_agent()
        with patch("app_factory.agents.qa_agent.subprocess.run",
                    side_effect=subprocess.TimeoutExpired(cmd="ruff", timeout=120)):
            result = agent.run_linter("/worktree/path")

        assert result["passed"] is False
        assert "timed out" in result["errors"][0]


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

class TestRunTests:
    def test_all_tests_pass(self):
        agent, _, _ = _build_agent()
        pytest_output = (
            "tests/test_foo.py::test_one PASSED\n"
            "tests/test_foo.py::test_two PASSED\n"
            "========================= 2 passed =========================\n"
        )
        mock_result = subprocess.CompletedProcess(
            args=["python", "-m", "pytest"],
            returncode=0,
            stdout=pytest_output,
            stderr="",
        )
        with patch("app_factory.agents.qa_agent.subprocess.run", return_value=mock_result):
            result = agent.run_tests("/worktree/path")

        assert result["passed"] is True
        assert result["total"] == 2
        assert result["failures"] == 0
        assert result["errors"] == 0

    def test_some_tests_fail(self):
        agent, _, _ = _build_agent()
        pytest_output = (
            "tests/test_foo.py::test_one PASSED\n"
            "tests/test_foo.py::test_two FAILED\n"
            "=================== 1 failed, 1 passed ====================\n"
        )
        mock_result = subprocess.CompletedProcess(
            args=["python", "-m", "pytest"],
            returncode=1,
            stdout=pytest_output,
            stderr="",
        )
        with patch("app_factory.agents.qa_agent.subprocess.run", return_value=mock_result):
            result = agent.run_tests("/worktree/path")

        assert result["passed"] is False
        assert result["total"] == 2
        assert result["failures"] == 1

    def test_pytest_not_found(self):
        agent, _, _ = _build_agent()
        with patch("app_factory.agents.qa_agent.subprocess.run", side_effect=FileNotFoundError):
            result = agent.run_tests("/worktree/path")

        assert result["passed"] is False
        assert "pytest not found" in result["output"]

    def test_pytest_timeout(self):
        agent, _, _ = _build_agent()
        with patch("app_factory.agents.qa_agent.subprocess.run",
                    side_effect=subprocess.TimeoutExpired(cmd="pytest", timeout=300)):
            result = agent.run_tests("/worktree/path")

        assert result["passed"] is False
        assert "timed out" in result["output"]


# ---------------------------------------------------------------------------
# Code Review
# ---------------------------------------------------------------------------

class TestCodeReview:
    @pytest.mark.asyncio
    async def test_review_approved(self):
        agent, _, mock_client = _build_agent()
        mock_client.complete = AsyncMock(
            return_value=_make_review_response(APPROVED_REVIEW)
        )

        result = await agent.code_review("diff content", task={"id": "1", "title": "Add feature"})

        assert result["approved"] is True
        assert len(result["issues"]) == 1
        assert result["issues"][0]["severity"] == "info"
        assert result["summary"] != ""

    @pytest.mark.asyncio
    async def test_review_rejected(self):
        agent, _, mock_client = _build_agent()
        mock_client.complete = AsyncMock(
            return_value=_make_review_response(REJECTED_REVIEW)
        )

        result = await agent.code_review("diff with issues")

        assert result["approved"] is False
        assert len(result["issues"]) == 2
        assert result["issues"][0]["severity"] == "critical"
        assert "security" in result["summary"].lower()

    @pytest.mark.asyncio
    async def test_review_no_task_context(self):
        agent, _, mock_client = _build_agent()
        mock_client.complete = AsyncMock(
            return_value=_make_review_response(APPROVED_REVIEW)
        )

        result = await agent.code_review("diff content", task=None)

        assert result["approved"] is True
        mock_client.complete.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_review_loads_template(self):
        agent, _, mock_client = _build_agent()
        mock_client.complete = AsyncMock(
            return_value=_make_review_response(APPROVED_REVIEW)
        )

        await agent.code_review("some diff")

        call_args = mock_client.complete.call_args
        prompt_text = call_args.kwargs["prompt"]
        assert "Review Checklist" in prompt_text
        assert "OWASP" in prompt_text


# ---------------------------------------------------------------------------
# Merge to main
# ---------------------------------------------------------------------------

class TestMergeToMain:
    def test_merge_success(self):
        agent, mock_repo, _ = _build_agent()
        mock_repo.head.commit.hexsha = "abc123def456"

        result = agent.merge_to_main("/worktree/path", "42")

        assert result["success"] is True
        assert result["commit_sha"] == "abc123def456"
        mock_repo.git.checkout.assert_called_once_with("main")
        mock_repo.git.merge.assert_called_once_with(
            "--no-ff", "feature/task-42", m="Merge feature/task-42"
        )

    def test_merge_failure(self):
        agent, mock_repo, _ = _build_agent()
        mock_repo.git.merge.side_effect = gitmod.GitCommandError("merge", "conflict")

        result = agent.merge_to_main("/worktree/path", "42")

        assert result["success"] is False
        assert result["commit_sha"] is None


# ---------------------------------------------------------------------------
# Full pipeline: review_and_merge
# ---------------------------------------------------------------------------

class TestReviewAndMerge:
    @pytest.mark.asyncio
    async def test_happy_path(self):
        agent, mock_repo, mock_client = _build_agent()
        mock_repo.head.commit.hexsha = "merged123"

        mock_wt_repo = MagicMock()
        mock_wt_repo.git.diff.return_value = "diff --git a/file.py"

        with patch.object(agent, "rebase_onto_main", new_callable=AsyncMock,
                          return_value={"success": True, "conflicts": []}), \
             patch.object(agent, "run_linter",
                          return_value={"passed": True, "errors": [], "warnings": []}), \
             patch.object(agent, "run_tests",
                          return_value={"passed": True, "total": 5, "failures": 0, "errors": 0, "output": "ok"}), \
             patch.object(agent, "code_review", new_callable=AsyncMock,
                          return_value={"approved": True, "issues": [], "summary": "All good"}), \
             patch.object(agent, "merge_to_main",
                          return_value={"success": True, "commit_sha": "merged123"}), \
             patch("app_factory.agents.qa_agent.git.Repo", return_value=mock_wt_repo):

            result = await agent.review_and_merge("task-1", "/worktree/path")

        assert result["status"] == "merged"
        assert result["commit_sha"] == "merged123"

    @pytest.mark.asyncio
    async def test_rebase_failure(self):
        agent, _, _ = _build_agent()

        with patch.object(agent, "rebase_onto_main", new_callable=AsyncMock,
                          return_value={"success": False, "conflicts": ["file.py"]}):
            result = await agent.review_and_merge("task-2", "/worktree/path")

        assert result["status"] == "rebase_failed"
        assert "file.py" in result["conflicts"]
        assert result["retry_count"] == 1

    @pytest.mark.asyncio
    async def test_lint_failure(self):
        agent, _, _ = _build_agent()

        with patch.object(agent, "rebase_onto_main", new_callable=AsyncMock,
                          return_value={"success": True, "conflicts": []}), \
             patch.object(agent, "run_linter",
                          return_value={"passed": False, "errors": ["E501 line too long"], "warnings": []}):
            result = await agent.review_and_merge("task-3", "/worktree/path")

        assert result["status"] == "lint_failed"
        assert len(result["errors"]) == 1

    @pytest.mark.asyncio
    async def test_test_failure(self):
        agent, _, _ = _build_agent()
        mock_wt_repo = MagicMock()

        with patch.object(agent, "rebase_onto_main", new_callable=AsyncMock,
                          return_value={"success": True, "conflicts": []}), \
             patch.object(agent, "run_linter",
                          return_value={"passed": True, "errors": [], "warnings": []}), \
             patch.object(agent, "run_tests",
                          return_value={"passed": False, "total": 3, "failures": 1, "errors": 0, "output": "FAILED"}):
            result = await agent.review_and_merge("task-4", "/worktree/path")

        assert result["status"] == "tests_failed"
        assert result["failures"] == 1

    @pytest.mark.asyncio
    async def test_review_failure(self):
        agent, _, _ = _build_agent()
        mock_wt_repo = MagicMock()
        mock_wt_repo.git.diff.return_value = "diff"

        with patch.object(agent, "rebase_onto_main", new_callable=AsyncMock,
                          return_value={"success": True, "conflicts": []}), \
             patch.object(agent, "run_linter",
                          return_value={"passed": True, "errors": [], "warnings": []}), \
             patch.object(agent, "run_tests",
                          return_value={"passed": True, "total": 3, "failures": 0, "errors": 0, "output": "ok"}), \
             patch.object(agent, "code_review", new_callable=AsyncMock,
                          return_value={"approved": False, "issues": [{"severity": "critical", "description": "vuln"}], "summary": "Bad"}), \
             patch("app_factory.agents.qa_agent.git.Repo", return_value=mock_wt_repo):
            result = await agent.review_and_merge("task-5", "/worktree/path")

        assert result["status"] == "review_failed"
        assert len(result["issues"]) == 1


# ---------------------------------------------------------------------------
# Parse test results
# ---------------------------------------------------------------------------

class TestParseTestResults:
    def test_all_passed(self):
        agent, _, _ = _build_agent()
        output = "========================= 5 passed =========================\n"
        result = agent.parse_test_results(output)
        assert result["passed"] is True
        assert result["total"] == 5
        assert result["failures"] == 0
        assert result["errors"] == 0

    def test_mixed_results(self):
        agent, _, _ = _build_agent()
        output = "================ 1 failed, 4 passed, 1 error ================\n"
        result = agent.parse_test_results(output)
        assert result["passed"] is False
        assert result["total"] == 6
        assert result["failures"] == 1
        assert result["errors"] == 1

    def test_all_failed(self):
        agent, _, _ = _build_agent()
        output = "========================= 3 failed =========================\n"
        result = agent.parse_test_results(output)
        assert result["passed"] is False
        assert result["total"] == 3
        assert result["failures"] == 3

    def test_no_tests(self):
        agent, _, _ = _build_agent()
        output = "no tests ran\n"
        result = agent.parse_test_results(output)
        assert result["passed"] is False
        assert result["total"] == 0

    def test_errors_only(self):
        agent, _, _ = _build_agent()
        output = "========================= 2 error =========================\n"
        result = agent.parse_test_results(output)
        assert result["passed"] is False
        assert result["errors"] == 2


# ---------------------------------------------------------------------------
# Retry counter
# ---------------------------------------------------------------------------

class TestRetryCounter:
    def test_initial_count_zero(self):
        agent, _, _ = _build_agent()
        assert agent.get_retry_count("task-1") == 0

    def test_increment_and_get(self):
        agent, _, _ = _build_agent()
        agent._increment_retry("task-1")
        assert agent.get_retry_count("task-1") == 1
        agent._increment_retry("task-1")
        assert agent.get_retry_count("task-1") == 2

    def test_separate_task_counters(self):
        agent, _, _ = _build_agent()
        agent._increment_retry("task-1")
        agent._increment_retry("task-1")
        agent._increment_retry("task-2")
        assert agent.get_retry_count("task-1") == 2
        assert agent.get_retry_count("task-2") == 1

    @pytest.mark.asyncio
    async def test_pipeline_failure_increments_counter(self):
        agent, _, _ = _build_agent()

        with patch.object(agent, "rebase_onto_main", new_callable=AsyncMock,
                          return_value={"success": False, "conflicts": ["a.py"]}):
            await agent.review_and_merge("task-99", "/wt")

        assert agent.get_retry_count("task-99") == 1

        with patch.object(agent, "rebase_onto_main", new_callable=AsyncMock,
                          return_value={"success": False, "conflicts": ["a.py"]}):
            await agent.review_and_merge("task-99", "/wt")

        assert agent.get_retry_count("task-99") == 2


# ---------------------------------------------------------------------------
# Review response parsing
# ---------------------------------------------------------------------------

class TestParseReviewResponse:
    def test_approved_with_info(self):
        agent, _, _ = _build_agent()
        result = agent._parse_review_response(APPROVED_REVIEW)
        assert result["approved"] is True
        assert len(result["issues"]) == 1
        assert result["issues"][0]["severity"] == "info"
        assert "good" in result["summary"].lower()

    def test_rejected_with_critical(self):
        agent, _, _ = _build_agent()
        result = agent._parse_review_response(REJECTED_REVIEW)
        assert result["approved"] is False
        assert len(result["issues"]) == 2
        assert result["issues"][0]["severity"] == "critical"
        assert result["issues"][1]["severity"] == "warning"

    def test_empty_response(self):
        agent, _, _ = _build_agent()
        result = agent._parse_review_response("")
        assert result["approved"] is False
        assert result["issues"] == []
        assert result["summary"] == ""
