"""Tests for architect planner (prompt construction and response parsing)."""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from cleave.architect.config import ArchitectConfig
from cleave.architect.errors import PlanningError
from cleave.architect.planner import (
    build_architect_prompt,
    parse_plan_response,
)


def _two_phases(**overrides):
    """Helper to build a valid 2-phase plan with optional overrides on phase 0."""
    phase_a = {"id": "phase-1-types", "label": "Define types", "description": "Create type definitions"}
    phase_a.update(overrides)
    phase_b = {"id": "phase-2-impl", "label": "Implement", "description": "Build the implementation"}
    return {"phases": [phase_a, phase_b], "rationale": "Types first, then implementation"}


class TestParsePlanResponse:
    def test_valid_json(self) -> None:
        response = json.dumps({
            "phases": [
                {"id": "phase-1-types", "label": "Define types", "description": "Create types"},
                {"id": "phase-2-impl", "label": "Implement", "description": "Build it"},
            ],
            "rationale": "Types first",
        })
        result = parse_plan_response(response)
        assert len(result["phases"]) == 2
        assert result["phases"][0]["id"] == "phase-1-types"

    def test_strips_markdown_fences(self) -> None:
        inner = json.dumps(_two_phases())
        response = f"```json\n{inner}\n```"
        result = parse_plan_response(response)
        assert len(result["phases"]) == 2

    def test_extracts_json_from_text(self) -> None:
        payload = json.dumps(_two_phases())
        response = f"Here is my plan:\n{payload}\nThat should work."
        result = parse_plan_response(response)
        assert result["phases"][0]["id"] == "phase-1-types"

    def test_normalizes_ids(self) -> None:
        response = json.dumps(_two_phases(id="Phase 1 Types"))
        result = parse_plan_response(response)
        assert result["phases"][0]["id"] == "phase-1-types"

    def test_truncates_long_ids(self) -> None:
        response = json.dumps(_two_phases(id="a" * 100))
        result = parse_plan_response(response)
        assert len(result["phases"][0]["id"]) <= 50

    def test_auto_generates_id_from_label(self) -> None:
        phase_a = {"label": "Define types", "description": "Create types"}
        phase_b = {"id": "phase-2", "label": "Build", "description": "Build it"}
        response = json.dumps({"phases": [phase_a, phase_b]})
        result = parse_plan_response(response)
        assert "phase-1" in result["phases"][0]["id"]

    def test_rejects_missing_phases(self) -> None:
        response = json.dumps({"rationale": "No phases"})
        with pytest.raises(PlanningError, match="no 'phases'"):
            parse_plan_response(response)

    def test_rejects_single_phase(self) -> None:
        response = json.dumps({
            "phases": [{"id": "p1", "label": "Only one", "description": "Just one"}],
        })
        with pytest.raises(PlanningError, match="minimum is 2"):
            parse_plan_response(response)

    def test_rejects_non_dict_response(self) -> None:
        with pytest.raises(PlanningError, match="not a JSON object"):
            parse_plan_response("[1, 2, 3]")

    def test_rejects_missing_description(self) -> None:
        response = json.dumps({
            "phases": [
                {"id": "p1", "label": "No desc"},
                {"id": "p2", "label": "Valid", "description": "Has desc"},
            ],
        })
        with pytest.raises(PlanningError, match="missing 'description'"):
            parse_plan_response(response)

    def test_rejects_invalid_json(self) -> None:
        with pytest.raises(PlanningError, match="No JSON"):
            parse_plan_response("This is not JSON at all")

    def test_truncates_excess_phases(self) -> None:
        phases = [
            {"id": f"p{i}", "label": f"Phase {i}", "description": f"Desc {i}"}
            for i in range(15)
        ]
        response = json.dumps({"phases": phases})
        result = parse_plan_response(response, max_phases=5)
        assert len(result["phases"]) == 5

    def test_defaults_for_optional_fields(self) -> None:
        response = json.dumps(_two_phases())
        result = parse_plan_response(response)
        assert result["phases"][0]["phase_type"] == "cleave_run"
        assert result["phases"][0]["depends_on"] == []
        assert result["phases"][0]["success_criteria"] == []

    def test_validates_depends_on_references(self) -> None:
        response = json.dumps(_two_phases(depends_on=["nonexistent"]))
        result = parse_plan_response(response)
        # Unknown deps should be pruned
        assert result["phases"][0]["depends_on"] == []

    def test_strips_self_dependencies(self) -> None:
        response = json.dumps(_two_phases(depends_on=["phase-1-types"]))
        result = parse_plan_response(response)
        assert "phase-1-types" not in result["phases"][0]["depends_on"]

    def test_valid_depends_on_preserved(self) -> None:
        phases = {
            "phases": [
                {"id": "phase-1", "label": "First", "description": "Do first"},
                {"id": "phase-2", "label": "Second", "description": "Do second", "depends_on": ["phase-1"]},
            ],
        }
        result = parse_plan_response(json.dumps(phases))
        assert result["phases"][1]["depends_on"] == ["phase-1"]

    def test_cycle_detection_breaks_cycles(self) -> None:
        phases = {
            "phases": [
                {"id": "phase-1", "label": "A", "description": "A", "depends_on": ["phase-2"]},
                {"id": "phase-2", "label": "B", "description": "B", "depends_on": ["phase-1"]},
            ],
        }
        result = parse_plan_response(json.dumps(phases))
        # At least one should have deps cleared
        deps_0 = result["phases"][0]["depends_on"]
        deps_1 = result["phases"][1]["depends_on"]
        # Cycle should be broken — not both can depend on each other
        assert not (deps_0 and deps_1)


class TestBuildArchitectPrompt:
    def test_includes_directive(self, tmp_path: Path) -> None:
        (tmp_path / ".git").mkdir()
        config = ArchitectConfig(
            directive="Build auth system",
            repo_path=tmp_path,
        )
        prompt = build_architect_prompt(config)
        assert "Build auth system" in prompt

    def test_includes_success_criteria(self, tmp_path: Path) -> None:
        (tmp_path / ".git").mkdir()
        config = ArchitectConfig(
            directive="Build it",
            repo_path=tmp_path,
            success_criteria=["all tests pass", "no lint errors"],
        )
        prompt = build_architect_prompt(config)
        assert "all tests pass" in prompt
        assert "no lint errors" in prompt

    def test_includes_repo_name(self, tmp_path: Path) -> None:
        (tmp_path / ".git").mkdir()
        config = ArchitectConfig(directive="test", repo_path=tmp_path)
        prompt = build_architect_prompt(config)
        assert tmp_path.name in prompt

    def test_includes_claude_md(self, tmp_path: Path) -> None:
        (tmp_path / ".git").mkdir()
        (tmp_path / "CLAUDE.md").write_text("# Project Guide\nUse pytest for tests.")
        config = ArchitectConfig(directive="test", repo_path=tmp_path)
        prompt = build_architect_prompt(config)
        assert "Project Guide" in prompt

    def test_includes_budget_info(self, tmp_path: Path) -> None:
        (tmp_path / ".git").mkdir()
        config = ArchitectConfig(
            directive="test", repo_path=tmp_path,
            phase_budget_usd=75.0,
        )
        prompt = build_architect_prompt(config)
        assert "$75.00" in prompt
