"""Tests for project directory lifecycle."""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from pact.project import ProjectManager
from pact.schemas import (
    ComponentContract,
    ContractTestSuite,
    DecompositionNode,
    DecompositionTree,
    DesignDocument,
    FieldSpec,
    FunctionContract,
    InterviewResult,
    RunState,
    TestCase,
)


@pytest.fixture
def tmp_project(tmp_path: Path) -> ProjectManager:
    """Create and init a temporary project."""
    pm = ProjectManager(tmp_path / "test-project")
    pm.init()
    return pm


class TestProjectInit:
    def test_creates_directories(self, tmp_project: ProjectManager):
        assert tmp_project.project_dir.exists()
        # Visible project directories
        assert (tmp_project.project_dir / "contracts").exists()
        assert (tmp_project.project_dir / "src").exists()
        assert (tmp_project.project_dir / "tests").exists()
        assert (tmp_project.project_dir / "decomposition").exists()
        assert (tmp_project.project_dir / "learnings").exists()
        # Ephemeral run state directories
        assert (tmp_project.project_dir / ".pact").exists()
        assert (tmp_project.project_dir / ".pact" / "contracts").exists()
        assert (tmp_project.project_dir / ".pact" / "implementations").exists()
        assert (tmp_project.project_dir / ".pact" / "compositions").exists()

    def test_creates_task_template(self, tmp_project: ProjectManager):
        assert tmp_project.task_path.exists()
        assert "Task" in tmp_project.task_path.read_text()

    def test_creates_sops_template(self, tmp_project: ProjectManager):
        assert tmp_project.sops_path.exists()
        assert "Operating Procedures" in tmp_project.sops_path.read_text()

    def test_creates_config(self, tmp_project: ProjectManager):
        assert tmp_project.config_path.exists()

    def test_creates_design_doc(self, tmp_project: ProjectManager):
        assert tmp_project.design_path.exists()

    def test_idempotent(self, tmp_project: ProjectManager):
        # Write custom content
        tmp_project.task_path.write_text("# My Task")
        # Re-init should not overwrite
        tmp_project.init()
        assert tmp_project.task_path.read_text() == "# My Task"


class TestTaskAndConfig:
    def test_load_task(self, tmp_project: ProjectManager):
        tmp_project.task_path.write_text("# Build pricing engine")
        task = tmp_project.load_task()
        assert "pricing engine" in task

    def test_load_task_missing(self, tmp_path: Path):
        pm = ProjectManager(tmp_path / "no-project")
        with pytest.raises(FileNotFoundError):
            pm.load_task()

    def test_load_sops(self, tmp_project: ProjectManager):
        sops = tmp_project.load_sops()
        assert "Operating Procedures" in sops

    def test_load_sops_missing(self, tmp_path: Path):
        pm = ProjectManager(tmp_path / "no-project")
        assert pm.load_sops() == ""

    def test_load_config(self, tmp_project: ProjectManager):
        config = tmp_project.load_config()
        assert config.budget == 10.00


class TestRunState:
    def test_create_and_save(self, tmp_project: ProjectManager):
        state = tmp_project.create_run()
        assert state.status == "active"
        tmp_project.save_state(state)
        assert tmp_project.has_state()

    def test_load_state(self, tmp_project: ProjectManager):
        state = tmp_project.create_run()
        tmp_project.save_state(state)
        loaded = tmp_project.load_state()
        assert loaded.id == state.id

    def test_load_state_missing(self, tmp_project: ProjectManager):
        with pytest.raises(FileNotFoundError):
            tmp_project.load_state()

    def test_clear_state(self, tmp_project: ProjectManager):
        state = tmp_project.create_run()
        tmp_project.save_state(state)
        tmp_project.clear_state()
        assert not tmp_project.has_state()
        # Directories should be recreated
        assert (tmp_project.project_dir / ".pact" / "contracts").exists()


class TestAudit:
    def test_append_and_load(self, tmp_project: ProjectManager):
        tmp_project.append_audit("test_action", "some detail")
        entries = tmp_project.load_audit()
        assert len(entries) == 1
        assert entries[0]["action"] == "test_action"

    def test_multiple_entries(self, tmp_project: ProjectManager):
        tmp_project.append_audit("action1", "d1")
        tmp_project.append_audit("action2", "d2")
        entries = tmp_project.load_audit()
        assert len(entries) == 2

    def test_empty_audit(self, tmp_project: ProjectManager):
        entries = tmp_project.load_audit()
        assert entries == []


class TestDecomposition:
    def test_save_and_load_tree(self, tmp_project: ProjectManager):
        tree = DecompositionTree(
            root_id="root",
            nodes={
                "root": DecompositionNode(
                    component_id="root", name="Root", description="r",
                ),
            },
        )
        tmp_project.save_tree(tree)
        loaded = tmp_project.load_tree()
        assert loaded is not None
        assert loaded.root_id == "root"

    def test_load_tree_missing(self, tmp_project: ProjectManager):
        assert tmp_project.load_tree() is None

    def test_save_and_load_interview(self, tmp_project: ProjectManager):
        result = InterviewResult(
            risks=["risk1"],
            questions=["q1"],
        )
        tmp_project.save_interview(result)
        loaded = tmp_project.load_interview()
        assert loaded is not None
        assert loaded.risks == ["risk1"]

    def test_save_decisions(self, tmp_project: ProjectManager):
        decisions = [{"ambiguity": "auth", "decision": "JWT", "rationale": "simpler"}]
        tmp_project.save_decisions(decisions)
        path = tmp_project.project_dir / "decomposition" / "decisions.json"
        assert path.exists()


class TestContracts:
    def test_save_and_load_contract(self, tmp_project: ProjectManager):
        contract = ComponentContract(
            component_id="pricing",
            name="Pricing",
            description="Pricing engine",
            functions=[
                FunctionContract(
                    name="calc", description="d",
                    inputs=[FieldSpec(name="x", type_ref="str")],
                    output_type="float",
                ),
            ],
        )
        tmp_project.save_contract(contract)
        loaded = tmp_project.load_contract("pricing")
        assert loaded is not None
        assert loaded.name == "Pricing"

    def test_saves_history(self, tmp_project: ProjectManager):
        contract = ComponentContract(
            component_id="pricing",
            name="Pricing",
            description="d",
        )
        tmp_project.save_contract(contract)
        history_dir = tmp_project.project_dir / "contracts" / "pricing" / "history"
        assert any(history_dir.iterdir())

    def test_load_all_contracts(self, tmp_project: ProjectManager):
        for cid in ["a", "b", "c"]:
            c = ComponentContract(component_id=cid, name=cid.upper(), description="d")
            tmp_project.save_contract(c)
        all_c = tmp_project.load_all_contracts()
        assert len(all_c) == 3

    def test_load_contract_missing(self, tmp_project: ProjectManager):
        assert tmp_project.load_contract("nonexistent") is None


class TestTestSuites:
    def test_save_and_load(self, tmp_project: ProjectManager):
        suite = ContractTestSuite(
            component_id="pricing",
            contract_version=1,
            test_cases=[
                TestCase(id="t1", description="d", function="f", category="happy_path"),
            ],
            generated_code="def test_it(): pass",
        )
        tmp_project.save_test_suite(suite)
        loaded = tmp_project.load_test_suite("pricing")
        assert loaded is not None
        assert len(loaded.test_cases) == 1

    def test_saves_code_file(self, tmp_project: ProjectManager):
        suite = ContractTestSuite(
            component_id="pricing",
            contract_version=1,
            generated_code="def test_it(): pass",
        )
        tmp_project.save_test_suite(suite)
        code_path = tmp_project.test_code_path("pricing")
        assert code_path.exists()
        assert "test_it" in code_path.read_text()

    def test_load_all(self, tmp_project: ProjectManager):
        for cid in ["a", "b"]:
            s = ContractTestSuite(
                component_id=cid, contract_version=1,
                test_cases=[TestCase(id="t", description="d", function="f", category="happy_path")],
            )
            tmp_project.save_test_suite(s)
        all_s = tmp_project.load_all_test_suites()
        assert len(all_s) == 2


class TestGoodhartSuites:
    def test_save_and_load_roundtrip(self, tmp_project: ProjectManager):
        suite = ContractTestSuite(
            component_id="pricing",
            contract_version=1,
            test_cases=[
                TestCase(id="t1", description="commutative property for all inputs",
                         function="add", category="invariant"),
            ],
            generated_code="def test_goodhart_commutative(): pass",
        )
        tmp_project.save_goodhart_suite(suite)
        loaded = tmp_project.load_goodhart_suite("pricing")
        assert loaded is not None
        assert len(loaded.test_cases) == 1
        assert loaded.component_id == "pricing"

    def test_saves_code_file(self, tmp_project: ProjectManager):
        suite = ContractTestSuite(
            component_id="pricing",
            contract_version=1,
            generated_code="def test_goodhart_it(): pass",
        )
        tmp_project.save_goodhart_suite(suite)
        code_path = tmp_project.goodhart_test_code_path("pricing")
        assert code_path.exists()
        assert "test_goodhart_it" in code_path.read_text()

    def test_goodhart_directory_is_separate_from_tests(self, tmp_project: ProjectManager):
        suite = ContractTestSuite(
            component_id="pricing",
            contract_version=1,
            generated_code="def test_goodhart_it(): pass",
        )
        tmp_project.save_goodhart_suite(suite)
        # Goodhart goes to tests/<cid>/goodhart/
        goodhart_dir = tmp_project.project_dir / "tests" / "pricing" / "goodhart"
        assert goodhart_dir.exists()
        # Goodhart suite JSON is separate from visible contract_test_suite.json
        assert not (tmp_project.project_dir / "tests" / "pricing" / "contract_test_suite.json").exists()

    def test_isolation_load_all_test_suites_excludes_goodhart(self, tmp_project: ProjectManager):
        """Critical: load_all_test_suites must NOT include Goodhart suites."""
        visible = ContractTestSuite(
            component_id="pricing", contract_version=1,
            test_cases=[TestCase(id="t1", description="d", function="f", category="happy_path")],
            generated_code="def test_visible(): pass",
        )
        goodhart = ContractTestSuite(
            component_id="pricing", contract_version=1,
            test_cases=[TestCase(id="g1", description="d", function="f", category="invariant")],
            generated_code="def test_goodhart_hidden(): pass",
        )
        tmp_project.save_test_suite(visible)
        tmp_project.save_goodhart_suite(goodhart)

        all_visible = tmp_project.load_all_test_suites()
        assert "pricing" in all_visible
        # The visible suite should have the visible test, not the goodhart one
        assert any(tc.id == "t1" for tc in all_visible["pricing"].test_cases)
        assert not any(tc.id == "g1" for tc in all_visible["pricing"].test_cases)

    def test_load_all_goodhart_suites(self, tmp_project: ProjectManager):
        for cid in ["a", "b"]:
            s = ContractTestSuite(
                component_id=cid, contract_version=1,
                test_cases=[TestCase(id="g1", description="d", function="f", category="invariant")],
                generated_code="def test_goodhart_it(): pass",
            )
            tmp_project.save_goodhart_suite(s)
        all_g = tmp_project.load_all_goodhart_suites()
        assert len(all_g) == 2

    def test_load_missing_returns_none(self, tmp_project: ProjectManager):
        assert tmp_project.load_goodhart_suite("nonexistent") is None

    def test_goodhart_path_distinct_from_visible(self, tmp_project: ProjectManager):
        visible_path = tmp_project.test_code_path("pricing")
        goodhart_path = tmp_project.goodhart_test_code_path("pricing")
        assert visible_path != goodhart_path
        assert "tests" in str(visible_path)
        assert "goodhart" in str(goodhart_path)


class TestImplementations:
    def test_impl_dir(self, tmp_project: ProjectManager):
        d = tmp_project.impl_dir("pricing")
        assert d.exists()

    def test_impl_src_dir(self, tmp_project: ProjectManager):
        d = tmp_project.impl_src_dir("pricing")
        assert d.exists()
        assert d.name == "pricing"
        assert d.parent.name == "src"

    def test_save_metadata(self, tmp_project: ProjectManager):
        tmp_project.save_impl_metadata("pricing", {"attempt": 1})
        path = tmp_project.impl_dir("pricing") / "metadata.json"
        assert path.exists()


class TestLearnings:
    def test_append_and_load(self, tmp_project: ProjectManager):
        tmp_project.append_learning({"lesson": "Use Result types", "category": "pattern"})
        entries = tmp_project.load_learnings()
        assert len(entries) == 1

    def test_empty(self, tmp_project: ProjectManager):
        assert tmp_project.load_learnings() == []


class TestDesignDoc:
    def test_save_and_load(self, tmp_project: ProjectManager):
        doc = DesignDocument(
            project_id="test",
            title="Test Design",
            summary="A test",
        )
        tmp_project.save_design_doc(doc)
        loaded = tmp_project.load_design_doc()
        assert loaded is not None
        assert loaded.title == "Test Design"

    def test_load_missing(self, tmp_project: ProjectManager):
        assert tmp_project.load_design_doc() is None


class TestTaskListPersistence:
    def test_save_and_load_roundtrip(self, tmp_project: ProjectManager):
        from pact.schemas_tasks import TaskItem, TaskList, TaskPhase, TaskStatus

        tl = TaskList(
            project_id="test",
            tasks=[
                TaskItem(id="T001", phase=TaskPhase.setup, description="Init"),
                TaskItem(id="T002", phase=TaskPhase.component, description="Build",
                         component_id="auth", status=TaskStatus.completed),
            ],
        )
        tmp_project.save_task_list(tl)
        loaded = tmp_project.load_task_list()
        assert loaded is not None
        assert loaded.project_id == "test"
        assert loaded.total == 2
        assert loaded.completed == 1

    def test_saves_json_file(self, tmp_project: ProjectManager):
        from pact.schemas_tasks import TaskList

        tl = TaskList(project_id="test")
        tmp_project.save_task_list(tl)
        assert tmp_project.tasks_json_path.exists()

    def test_saves_markdown_file(self, tmp_project: ProjectManager):
        from pact.schemas_tasks import TaskList

        tl = TaskList(project_id="test")
        tmp_project.save_task_list(tl)
        assert tmp_project.tasks_md_path.exists()
        md = tmp_project.tasks_md_path.read_text()
        assert "# TASKS" in md

    def test_load_missing(self, tmp_project: ProjectManager):
        assert tmp_project.load_task_list() is None

    def test_paths(self, tmp_project: ProjectManager):
        assert tmp_project.tasks_json_path.name == "tasks.json"
        assert tmp_project.tasks_md_path.name == "TASKS.md"


class TestAnalysisPersistence:
    def test_save_and_load_roundtrip(self, tmp_project: ProjectManager):
        from pact.schemas_tasks import (
            AnalysisFinding, AnalysisReport, FindingCategory, FindingSeverity,
        )

        report = AnalysisReport(
            project_id="test",
            findings=[
                AnalysisFinding(
                    id="F001", severity=FindingSeverity.error,
                    category=FindingCategory.coverage_gap,
                    description="Missing contract",
                ),
            ],
            summary="1 error",
        )
        tmp_project.save_analysis(report)
        loaded = tmp_project.load_analysis()
        assert loaded is not None
        assert loaded.project_id == "test"
        assert len(loaded.findings) == 1
        assert loaded.summary == "1 error"

    def test_load_missing(self, tmp_project: ProjectManager):
        assert tmp_project.load_analysis() is None

    def test_path(self, tmp_project: ProjectManager):
        assert tmp_project.analysis_path.name == "analysis.json"


class TestChecklistPersistence:
    def test_save_and_load_roundtrip(self, tmp_project: ProjectManager):
        from pact.schemas_tasks import (
            ChecklistCategory, ChecklistItem, RequirementsChecklist,
        )

        cl = RequirementsChecklist(
            project_id="test",
            items=[
                ChecklistItem(
                    id="C001", category=ChecklistCategory.requirements,
                    question="Is req clear?", satisfied=True,
                ),
                ChecklistItem(
                    id="C002", category=ChecklistCategory.edge_cases,
                    question="Edge case?",
                ),
            ],
        )
        tmp_project.save_checklist(cl)
        loaded = tmp_project.load_checklist()
        assert loaded is not None
        assert loaded.project_id == "test"
        assert len(loaded.items) == 2
        assert loaded.satisfied_count == 1
        assert loaded.unanswered == 1

    def test_load_missing(self, tmp_project: ProjectManager):
        assert tmp_project.load_checklist() is None

    def test_path(self, tmp_project: ProjectManager):
        assert tmp_project.checklist_path.name == "checklist.json"
