"""Tests for report module - .cloven.md session report generation."""

from __future__ import annotations

from pathlib import Path

import pytest

from cleave.core.file_utils import atomic_write_text
from cleave.core.report import (
    generate_cloven_report,
    get_accumulated_reports,
    write_cloven_report,
    _generate_calibration_insight,
)
from cleave.core.settings import CleaveSettings
from cleave.core.yaml_utils import to_yaml


class TestGenerateClovenReport:
    """Tests for generate_cloven_report function."""

    @pytest.fixture
    def workspace(self, tmp_path: Path) -> Path:
        """Create a workspace with manifest and task files."""
        workspace = tmp_path / "test-workspace"
        workspace.mkdir()

        # Create manifest with complete assessment, ancestry, intent, children
        manifest = {
            "root_directive": "Implement user authentication with OAuth2",
            "intent": {
                "goal": "Add OAuth2 authentication to the application",
                "success_criteria": [
                    "Users can log in via OAuth2 providers",
                    "Tokens are securely stored and validated",
                ],
                "constraints": ["Must support Google and GitHub providers"],
                "out_of_scope": ["Multi-factor authentication"],
            },
            "ancestry": {
                "depth": 0,
                "node_id": "root",
                "parent_chain": [],
                "remaining_budget": 5,
            },
            "children": [
                {"id": 0, "label": "Backend", "depth": 1},
                {"id": 1, "label": "Frontend", "depth": 1},
            ],
            "assessment": {
                "complexity": 4.5,
                "decision": "cleave",
                "pattern": "Authentication System",
                "confidence": 0.85,
                "systems": 2,
                "modifiers": ["state_coordination", "security_critical"],
                "method": "fast-path",
                "reasoning": "Pattern matched with high confidence",
            },
        }
        atomic_write_text(workspace / "manifest.yaml", to_yaml(manifest))

        # Create siblings.yaml
        siblings = {
            "coordination": {
                "mode": "parallel",
                "depth": 1,
                "parent_node": "root",
            },
            "siblings": [
                {"id": 0, "label": "Backend", "status": "success"},
                {"id": 1, "label": "Frontend", "status": "success"},
            ],
        }
        atomic_write_text(workspace / "siblings.yaml", to_yaml(siblings))

        # Create metrics.yaml
        metrics = {
            "telemetry": {
                "ancestry_access_count": 5,
                "sibling_access_count": 3,
                "intent_reference_count": 2,
                "cache_hits": 10,
                "cache_misses": 2,
                "fast_path_hits": 1,
                "sequential_fallback": 0,
            }
        }
        atomic_write_text(workspace / "metrics.yaml", to_yaml(metrics))

        return workspace

    def test_includes_directive_summary(self, workspace: Path) -> None:
        """Report should include the root directive."""
        aggregated = {
            "rollup_status": "SUCCESS",
            "task_count": 2,
            "by_task": [],
            "verification_summary": [],
            "all_decisions": [],
            "all_interfaces": [],
            "all_assumptions": [],
        }
        report = generate_cloven_report(
            workspace_path=workspace,
            aggregated=aggregated,
            conflicts=[],
            extracted_effort=[],
        )

        assert "Implement user authentication" in report
        assert "**Directive:**" in report

    def test_includes_prediction_metrics(self, workspace: Path) -> None:
        """Report should include assessment prediction details."""
        aggregated = self._make_aggregated()
        report = generate_cloven_report(
            workspace_path=workspace,
            aggregated=aggregated,
            conflicts=[],
            extracted_effort=[],
        )

        assert "Authentication System" in report
        assert "4.5" in report
        assert "85%" in report
        assert "cleave" in report

    def test_includes_execution_outcome(self, workspace: Path) -> None:
        """Report should include execution results."""
        aggregated = self._make_aggregated(
            by_task=[
                {"id": 0, "status": "SUCCESS", "file_count": 3, "verified": True, "interface_count": 1, "summary": "Backend done", "alignment": {}},
                {"id": 1, "status": "SUCCESS", "file_count": 2, "verified": False, "interface_count": 0, "summary": "Frontend done", "alignment": {}},
            ]
        )
        report = generate_cloven_report(
            workspace_path=workspace,
            aggregated=aggregated,
            conflicts=[],
            extracted_effort=[],
        )

        assert "SUCCESS" in report
        assert "| 0 | SUCCESS |" in report
        assert "| 1 | SUCCESS |" in report

    def test_includes_accuracy_analysis_with_effort(self, workspace: Path) -> None:
        """Report should calculate accuracy when effort is provided."""
        aggregated = self._make_aggregated()
        report = generate_cloven_report(
            workspace_path=workspace,
            aggregated=aggregated,
            conflicts=[],
            extracted_effort=[],
            explicit_effort="high",
        )

        assert "Accuracy Score" in report
        assert "high" in report
        assert "explicit override" in report

    def test_infers_effort_from_extractions(self, workspace: Path) -> None:
        """Report should use inferred effort when no explicit override."""
        aggregated = self._make_aggregated()
        extracted = [
            {"task_file": "0-task.md", "inferred_effort": "medium"},
            {"task_file": "1-task.md", "inferred_effort": "medium"},
        ]
        report = generate_cloven_report(
            workspace_path=workspace,
            aggregated=aggregated,
            conflicts=[],
            extracted_effort=extracted,
        )

        assert "medium" in report
        assert "inferred from task results" in report

    def test_includes_conflict_summary(self, workspace: Path) -> None:
        """Report should summarize conflicts."""
        aggregated = self._make_aggregated(rollup_status="PARTIAL")

        class FakeConflict:
            type = "file_overlap"

        report = generate_cloven_report(
            workspace_path=workspace,
            aggregated=aggregated,
            conflicts=[FakeConflict(), FakeConflict()],
            extracted_effort=[],
        )

        assert "2" in report  # Conflict count
        assert "file_overlap" in report

    def test_includes_calibration_insight(self, workspace: Path) -> None:
        """Report should provide actionable calibration guidance."""
        aggregated = self._make_aggregated()
        report = generate_cloven_report(
            workspace_path=workspace,
            aggregated=aggregated,
            conflicts=[],
            extracted_effort=[],
            explicit_effort="high",
        )

        assert "Calibration Insight" in report

    def test_includes_evidence_block(self, workspace: Path) -> None:
        """Report should include structured evidence for future sessions."""
        aggregated = self._make_aggregated()
        report = generate_cloven_report(
            workspace_path=workspace,
            aggregated=aggregated,
            conflicts=[],
            extracted_effort=[],
            explicit_effort="medium",
        )

        assert "Evidence for Future Sessions" in report
        assert "Previous cleave on similar directive" in report

    def test_includes_github_issue_template(self, workspace: Path) -> None:
        """Report should include GitHub issue template."""
        aggregated = self._make_aggregated()
        report = generate_cloven_report(
            workspace_path=workspace,
            aggregated=aggregated,
            conflicts=[],
            extracted_effort=[],
        )

        assert "GitHub Issue Template" in report
        assert "Ready-to-paste issue content" in report
        assert "### Summary" in report
        assert "### Next Steps" in report

    def test_includes_intent_section(self, workspace: Path) -> None:
        """Report should include full intent section."""
        aggregated = self._make_aggregated()
        report = generate_cloven_report(
            workspace_path=workspace,
            aggregated=aggregated,
            conflicts=[],
            extracted_effort=[],
        )

        assert "Directive & Intent" in report
        assert "Success Criteria" in report
        assert "OAuth2 authentication" in report

    def test_includes_ancestry_context(self, workspace: Path) -> None:
        """Report should include ancestry context."""
        aggregated = self._make_aggregated()
        report = generate_cloven_report(
            workspace_path=workspace,
            aggregated=aggregated,
            conflicts=[],
            extracted_effort=[],
        )

        assert "Ancestry Context" in report
        assert "Depth 0" in report
        assert "root" in report

    def test_includes_telemetry(self, workspace: Path) -> None:
        """Report should include telemetry data."""
        aggregated = self._make_aggregated()
        report = generate_cloven_report(
            workspace_path=workspace,
            aggregated=aggregated,
            conflicts=[],
            extracted_effort=[],
        )

        assert "Telemetry" in report
        assert "Ancestry Access" in report or "ancestry_access" in report.lower()

    def test_handles_missing_manifest(self, tmp_path: Path) -> None:
        """Report should handle missing manifest gracefully."""
        workspace = tmp_path / "empty-workspace"
        workspace.mkdir()

        aggregated = self._make_aggregated()
        report = generate_cloven_report(
            workspace_path=workspace,
            aggregated=aggregated,
            conflicts=[],
            extracted_effort=[],
        )

        assert "[unknown]" in report or "[not specified]" in report

    @staticmethod
    def _make_aggregated(
        rollup_status="SUCCESS",
        task_count=2,
        by_task=None,
    ) -> dict:
        """Helper to create aggregated dict with all required fields."""
        if by_task is None:
            by_task = []
        return {
            "rollup_status": rollup_status,
            "task_count": task_count,
            "by_task": by_task,
            "verification_summary": [],
            "all_decisions": [],
            "all_interfaces": [],
            "all_assumptions": [],
        }


class TestGenerateCalibrationInsight:
    """Tests for _generate_calibration_insight helper."""

    def test_accurate_prediction_no_adjustment(self) -> None:
        """High accuracy should suggest no adjustment needed."""
        insight = _generate_calibration_insight(
            predicted_complexity=4.5,
            actual_effort="high",
            pattern_matched="Authentication System",
            accuracy_score=0.95,
            systems_count=2,
            modifiers_applied=["security"],
        )

        assert "well-calibrated" in insight
        assert "No adjustment" in insight

    def test_under_prediction_suggests_increase(self) -> None:
        """Low complexity prediction with high effort should suggest increase."""
        insight = _generate_calibration_insight(
            predicted_complexity=2.0,
            actual_effort="high",
            pattern_matched="Data Pipeline",
            accuracy_score=0.3,
            systems_count=1,
            modifiers_applied=[],
        )

        assert "Under-prediction" in insight
        assert "increasing" in insight.lower()
        assert "Data Pipeline" in insight

    def test_over_prediction_suggests_decrease(self) -> None:
        """High complexity prediction with low effort should suggest decrease."""
        insight = _generate_calibration_insight(
            predicted_complexity=5.0,
            actual_effort="low",
            pattern_matched="CRUD Operation",
            accuracy_score=0.2,
            systems_count=3,
            modifiers_applied=["state_coordination"],
        )

        assert "Over-prediction" in insight
        assert "decreasing" in insight.lower()

    def test_handles_no_accuracy(self) -> None:
        """Should handle None accuracy gracefully."""
        insight = _generate_calibration_insight(
            predicted_complexity=3.0,
            actual_effort="unknown",
            pattern_matched=None,
            accuracy_score=None,
            systems_count=0,
            modifiers_applied=[],
        )

        assert "Insufficient data" in insight


class TestWriteClovenReport:
    """Tests for write_cloven_report function."""

    def test_writes_report_to_workspace(self, tmp_path: Path, monkeypatch) -> None:
        """Should write .cloven.md to workspace directory."""
        workspace = tmp_path / "workspace"
        workspace.mkdir()

        # Create minimal manifest
        manifest = {"root_directive": "Test directive"}
        atomic_write_text(workspace / "manifest.yaml", to_yaml(manifest))

        # Mock settings to use workspace naming (legacy)
        mock_settings = CleaveSettings(cloven_report_naming="workspace")
        monkeypatch.setattr("cleave.core.report.load_settings", lambda: mock_settings)

        aggregated = TestGenerateClovenReport._make_aggregated(task_count=1)
        report_path = write_cloven_report(
            workspace_path=workspace,
            aggregated=aggregated,
            conflicts=[],
            extracted_effort=[],
        )

        assert report_path.exists()
        assert report_path.name == ".cloven.md"
        assert "Cleave Session Report" in report_path.read_text()


class TestGetAccumulatedReports:
    """Tests for get_accumulated_reports function."""

    def test_finds_all_cloven_reports(self, tmp_path: Path) -> None:
        """Should find all .cloven.md files in project."""
        # Create nested structure with multiple reports
        (tmp_path / "workspace1").mkdir()
        (tmp_path / "workspace2").mkdir()
        (tmp_path / "nested" / "workspace3").mkdir(parents=True)

        report_content = """# Cleave Session Report
**Directive:** Test directive
| Pattern Matched | test-pattern |
| Accuracy Score | 85% |
| Actual Effort | medium (inferred) |
"""
        for ws in ["workspace1", "workspace2", "nested/workspace3"]:
            atomic_write_text(tmp_path / ws / ".cloven.md", report_content)

        reports = get_accumulated_reports(tmp_path)

        assert len(reports) == 3

    def test_extracts_key_metrics(self, tmp_path: Path) -> None:
        """Should extract pattern, accuracy, and effort from reports."""
        workspace = tmp_path / "workspace"
        workspace.mkdir()

        report_content = """# Cleave Session Report
**Directive:** Implement user auth
| Pattern Matched | Authentication System |
| Accuracy Score | 92% |
| Actual Effort | high (explicit) |
"""
        atomic_write_text(workspace / ".cloven.md", report_content)

        reports = get_accumulated_reports(tmp_path)

        assert len(reports) == 1
        assert reports[0]["pattern"] == "Authentication System"
        assert reports[0]["accuracy"] == 0.92
        assert "high" in reports[0]["actual_effort"]

    def test_handles_empty_project(self, tmp_path: Path) -> None:
        """Should return empty list for project with no reports."""
        reports = get_accumulated_reports(tmp_path)
        assert reports == []


class TestReunifyIntegration:
    """Tests for .cloven.md generation during reunify."""

    @pytest.fixture
    def workspace_with_tasks(self, tmp_path: Path) -> Path:
        """Create a complete workspace with manifest and completed tasks."""
        workspace = tmp_path / "test-workspace"
        workspace.mkdir()

        manifest = {
            "root_directive": "Add logging to all API endpoints",
            "intent": {
                "goal": "Add logging to all API endpoints",
                "success_criteria": ["All API endpoints log requests"],
                "constraints": [],
                "out_of_scope": [],
            },
            "ancestry": {
                "depth": 0,
                "node_id": "root",
                "parent_chain": [],
                "remaining_budget": 5,
            },
            "children": [
                {"id": 0, "label": "Request logging", "depth": 1},
                {"id": 1, "label": "Response logging", "depth": 1},
            ],
            "assessment": {
                "complexity": 3.5,
                "decision": "cleave",
                "pattern": "Cross-Cutting Concern",
                "confidence": 0.75,
                "systems": 2,
                "modifiers": [],
                "method": "fast-path",
            },
        }
        atomic_write_text(workspace / "manifest.yaml", to_yaml(manifest))

        # Create completed task files
        task0 = """---
task_id: 0
---
# Task 0: Add request logging

## Result
**Status:** SUCCESS
**Summary:** Added request logging middleware. Straightforward implementation.
"""
        task1 = """---
task_id: 1
---
# Task 1: Add response logging

## Result
**Status:** SUCCESS
**Summary:** Added response logging. Minor complexity with async handlers.
"""
        atomic_write_text(workspace / "0-task.md", task0)
        atomic_write_text(workspace / "1-task.md", task1)

        return workspace

    def test_reunify_generates_cloven_report(self, workspace_with_tasks: Path, monkeypatch) -> None:
        """Reunify should generate .cloven.md when write_report=True."""
        from cleave.core.reunify import reunify_workspace

        # Mock settings to use workspace naming (legacy)
        mock_settings = CleaveSettings(cloven_report_naming="workspace")
        monkeypatch.setattr("cleave.core.report.load_settings", lambda: mock_settings)

        result = reunify_workspace(
            workspace_path=str(workspace_with_tasks),
            write_report=True,
        )

        report_path = workspace_with_tasks / ".cloven.md"
        assert report_path.exists(), "Expected .cloven.md to be generated"
        assert result.get("report_file") == str(report_path)

    def test_reunify_skips_report_when_disabled(self, workspace_with_tasks: Path) -> None:
        """Reunify should skip .cloven.md when write_report=False."""
        from cleave.core.reunify import reunify_workspace

        result = reunify_workspace(
            workspace_path=str(workspace_with_tasks),
            write_report=False,
        )

        report_path = workspace_with_tasks / ".cloven.md"
        assert not report_path.exists()
        assert result.get("report_file") is None
