"""Tests for cleave metrics module - feedback loop and calibration."""

from __future__ import annotations

from pathlib import Path

import pytest

from cleave.core.metrics import (
    calculate_accuracy,
    extract_outcomes_from_tasks,
    get_metrics_summary,
    hash_directive,
    infer_effort_from_result,
    record_assessment,
    update_assessment_outcome,
)
from cleave.core.yaml_utils import parse_yaml_simple


class TestHashDirective:
    """Tests for directive hashing (privacy preservation)."""

    def test_produces_16_char_hash(self) -> None:
        """Should produce a 16-character hex hash."""
        result = hash_directive("Add JWT authentication")
        assert len(result) == 16
        assert all(c in "0123456789abcdef" for c in result)

    def test_same_input_same_hash(self) -> None:
        """Same directive should produce same hash."""
        hash1 = hash_directive("Add user login")
        hash2 = hash_directive("Add user login")
        assert hash1 == hash2

    def test_different_input_different_hash(self) -> None:
        """Different directives should produce different hashes."""
        hash1 = hash_directive("Add user login")
        hash2 = hash_directive("Add user logout")
        assert hash1 != hash2


class TestInferEffortFromResult:
    """Tests for effort inference from task result text."""

    def test_detects_low_effort_keywords(self) -> None:
        """Should detect low effort from keywords."""
        result = infer_effort_from_result("This was a trivial fix, straightforward change.")
        assert result == "low"

    def test_detects_medium_effort_keywords(self) -> None:
        """Should detect medium effort from keywords."""
        result = infer_effort_from_result("Moderate complexity, touched several files.")
        assert result == "medium"

    def test_detects_high_effort_keywords(self) -> None:
        """Should detect high effort from keywords."""
        result = infer_effort_from_result("This was a significant undertaking with extensive changes.")
        assert result == "high"

    def test_falls_back_to_word_count_short(self) -> None:
        """Short results without keywords should default to low."""
        result = infer_effort_from_result("Fixed the bug.")
        assert result == "low"

    def test_falls_back_to_word_count_medium(self) -> None:
        """Medium-length results should infer medium effort."""
        # 50+ words without effort keywords
        text = " ".join(["word"] * 100)
        result = infer_effort_from_result(text)
        assert result == "medium"

    def test_falls_back_to_word_count_high(self) -> None:
        """Long results should infer high effort."""
        # 200+ words without effort keywords
        text = " ".join(["word"] * 250)
        result = infer_effort_from_result(text)
        assert result == "high"


class TestCalculateAccuracy:
    """Tests for accuracy calculation."""

    def test_perfect_match_low(self) -> None:
        """Complexity 2.0 with low effort should be perfect."""
        accuracy = calculate_accuracy(2.0, "low")
        assert accuracy == 1.0

    def test_perfect_match_medium(self) -> None:
        """Complexity 3.5 with medium effort should be perfect."""
        accuracy = calculate_accuracy(3.5, "medium")
        assert accuracy == 1.0

    def test_perfect_match_high(self) -> None:
        """Complexity 6.0 with high effort should be perfect."""
        accuracy = calculate_accuracy(6.0, "high")
        assert accuracy == 1.0

    def test_underestimate_penalty(self) -> None:
        """Predicting low when actual is high should penalize."""
        accuracy = calculate_accuracy(1.0, "high")
        assert accuracy < 1.0
        assert accuracy >= 0.0

    def test_overestimate_penalty(self) -> None:
        """Predicting high when actual is low should penalize."""
        accuracy = calculate_accuracy(8.0, "low")
        assert accuracy < 1.0
        assert accuracy >= 0.0

    def test_unknown_effort_neutral(self) -> None:
        """Unknown effort level should return neutral 0.5."""
        accuracy = calculate_accuracy(3.0, "unknown")
        assert accuracy == 0.5


class TestRecordAssessment:
    """Tests for assessment recording."""

    def test_creates_metrics_file(self, tmp_path: Path) -> None:
        """Should create metrics.yaml if it doesn't exist."""
        workspace = tmp_path / ".cleave"
        workspace.mkdir()

        record_assessment(
            workspace_path=workspace,
            directive="Add user authentication",
            complexity=4.5,
            decision="cleave",
            pattern="Authentication System",
            confidence=0.85,
            systems=2,
            modifiers=["security_critical"],
        )

        metrics_file = workspace / "metrics.yaml"
        assert metrics_file.exists()

    def test_appends_to_existing_metrics(self, tmp_path: Path) -> None:
        """Should append to existing assessments list."""
        workspace = tmp_path / ".cleave"
        workspace.mkdir()

        # First assessment
        record_assessment(
            workspace_path=workspace,
            directive="First task",
            complexity=2.0,
            decision="execute",
            pattern=None,
            confidence=0.0,
            systems=1,
            modifiers=[],
        )

        # Second assessment
        record_assessment(
            workspace_path=workspace,
            directive="Second task",
            complexity=5.0,
            decision="cleave",
            pattern="Full-Stack CRUD",
            confidence=0.82,
            systems=3,
            modifiers=["data_migration"],
        )

        metrics = parse_yaml_simple((workspace / "metrics.yaml").read_text())
        assert len(metrics["assessments"]) == 2

    def test_stores_prediction_data(self, tmp_path: Path) -> None:
        """Should store all prediction fields."""
        workspace = tmp_path / ".cleave"
        workspace.mkdir()

        record_assessment(
            workspace_path=workspace,
            directive="Add Stripe integration",
            complexity=6.0,
            decision="cleave",
            pattern="External Integration",
            confidence=0.88,
            systems=4,
            modifiers=["third_party_api", "error_handling"],
        )

        metrics = parse_yaml_simple((workspace / "metrics.yaml").read_text())
        assessment = metrics["assessments"][0]

        assert assessment["predicted_complexity"] == 6.0
        assert assessment["predicted_decision"] == "cleave"
        assert assessment["pattern_matched"] == "External Integration"
        assert assessment["pattern_confidence"] == 0.88
        assert assessment["systems"] == 4
        assert "third_party_api" in assessment["modifiers"]
        # Outcome fields should be None initially
        assert assessment["actual_effort"] is None
        assert assessment["accuracy_score"] is None


class TestUpdateAssessmentOutcome:
    """Tests for updating assessments with actual outcomes."""

    def test_updates_existing_assessment(self, tmp_path: Path) -> None:
        """Should update assessment with outcome data."""
        workspace = tmp_path / ".cleave"
        workspace.mkdir()

        # Record initial assessment
        record = record_assessment(
            workspace_path=workspace,
            directive="Test task",
            complexity=3.0,
            decision="execute",
            pattern=None,
            confidence=0.0,
            systems=2,
            modifiers=[],
        )

        # Update with outcome
        accuracy = update_assessment_outcome(
            workspace_path=workspace,
            directive_hash=record.directive_hash,
            actual_effort="medium",
            actual_success=True,
        )

        assert accuracy is not None
        assert accuracy == 1.0  # 3.0 is within medium range (2.0-5.0)

        # Verify file was updated
        metrics = parse_yaml_simple((workspace / "metrics.yaml").read_text())
        assessment = metrics["assessments"][0]
        assert assessment["actual_effort"] == "medium"
        assert assessment["actual_success"] is True
        assert assessment["accuracy_score"] == 1.0

    def test_returns_none_for_missing_record(self, tmp_path: Path) -> None:
        """Should return None when assessment not found."""
        workspace = tmp_path / ".cleave"
        workspace.mkdir()
        (workspace / "metrics.yaml").write_text("assessments: []")

        result = update_assessment_outcome(
            workspace_path=workspace,
            directive_hash="nonexistent",
            actual_effort="low",
            actual_success=True,
        )

        assert result is None


class TestExtractOutcomesFromTasks:
    """Tests for extracting outcomes from task result files."""

    def test_extracts_success_status(self, tmp_path: Path) -> None:
        """Should extract SUCCESS status from task files."""
        workspace = tmp_path / ".cleave"
        workspace.mkdir()

        (workspace / "0-task.md").write_text("""
# Task 0: Test

## Result

**Status:** SUCCESS

**Summary:** This was a straightforward implementation.

**Artifacts:**
- file.py
""")

        outcomes = extract_outcomes_from_tasks(workspace)

        assert len(outcomes) == 1
        assert outcomes[0]["status"] == "SUCCESS"
        assert outcomes[0]["success"] is True
        assert outcomes[0]["inferred_effort"] == "low"  # "straightforward"

    def test_extracts_failed_status(self, tmp_path: Path) -> None:
        """Should extract FAILED status from task files."""
        workspace = tmp_path / ".cleave"
        workspace.mkdir()

        (workspace / "0-task.md").write_text("""
# Task 0: Test

## Result

**Status:** FAILED

**Summary:** Encountered significant issues.

**Artifacts:**
""")

        outcomes = extract_outcomes_from_tasks(workspace)

        assert len(outcomes) == 1
        assert outcomes[0]["status"] == "FAILED"
        assert outcomes[0]["success"] is False

    def test_skips_pending_tasks(self, tmp_path: Path) -> None:
        """Should skip tasks that are still PENDING."""
        workspace = tmp_path / ".cleave"
        workspace.mkdir()

        (workspace / "0-task.md").write_text("""
# Task 0: Test

## Result

**Status:** PENDING

**Summary:**
""")

        outcomes = extract_outcomes_from_tasks(workspace)
        assert len(outcomes) == 0

    def test_extracts_multiple_tasks(self, tmp_path: Path) -> None:
        """Should extract outcomes from all task files."""
        workspace = tmp_path / ".cleave"
        workspace.mkdir()

        (workspace / "0-task.md").write_text("""
## Result
**Status:** SUCCESS
**Summary:** Quick fix.
**Artifacts:**
""")

        (workspace / "1-task.md").write_text("""
## Result
**Status:** SUCCESS
**Summary:** Moderate complexity work.
**Artifacts:**
""")

        (workspace / "2-task.md").write_text("""
## Result
**Status:** PARTIAL
**Summary:** Extensive changes required.
**Artifacts:**
""")

        outcomes = extract_outcomes_from_tasks(workspace)
        assert len(outcomes) == 3


class TestGetMetricsSummary:
    """Tests for metrics summary generation."""

    def test_returns_empty_for_no_metrics(self, tmp_path: Path) -> None:
        """Should return default values when no metrics exist."""
        summary = get_metrics_summary(tmp_path)

        assert summary["total_assessments"] == 0
        assert summary["average_accuracy"] == 0.0
        assert "No metrics data found" in summary["recommendations"][0]

    def test_calculates_average_accuracy(self, tmp_path: Path) -> None:
        """Should calculate average accuracy from outcomes."""
        workspace = tmp_path / ".cleave"
        workspace.mkdir()

        # Create metrics with outcomes
        (workspace / "metrics.yaml").write_text("""
assessments:
  - directive_hash: abc123
    predicted_complexity: 3.0
    pattern_matched: null
    accuracy_score: 1.0
  - directive_hash: def456
    predicted_complexity: 2.0
    pattern_matched: "Authentication System"
    accuracy_score: 0.8
  - directive_hash: ghi789
    predicted_complexity: 5.0
    pattern_matched: "Authentication System"
    accuracy_score: 0.6
""")

        summary = get_metrics_summary(workspace)

        assert summary["total_assessments"] == 3
        assert summary["assessments_with_outcomes"] == 3
        assert summary["average_accuracy"] == 0.8  # (1.0 + 0.8 + 0.6) / 3

    def test_calculates_pattern_accuracy(self, tmp_path: Path) -> None:
        """Should calculate per-pattern accuracy."""
        workspace = tmp_path / ".cleave"
        workspace.mkdir()

        (workspace / "metrics.yaml").write_text("""
assessments:
  - directive_hash: a
    pattern_matched: "Authentication System"
    accuracy_score: 0.9
  - directive_hash: b
    pattern_matched: "Authentication System"
    accuracy_score: 0.7
  - directive_hash: c
    pattern_matched: "Full-Stack CRUD"
    accuracy_score: 0.5
""")

        summary = get_metrics_summary(workspace)

        assert summary["pattern_accuracy"]["Authentication System"] == 0.8  # (0.9 + 0.7) / 2
        assert summary["pattern_accuracy"]["Full-Stack CRUD"] == 0.5

    def test_generates_recommendations(self, tmp_path: Path) -> None:
        """Should generate calibration recommendations for low accuracy patterns."""
        workspace = tmp_path / ".cleave"
        workspace.mkdir()

        (workspace / "metrics.yaml").write_text("""
assessments:
  - directive_hash: a
    pattern_matched: "Full-Stack CRUD"
    accuracy_score: 0.4
  - directive_hash: b
    pattern_matched: "Full-Stack CRUD"
    accuracy_score: 0.3
  - directive_hash: c
    pattern_matched: "Full-Stack CRUD"
    accuracy_score: 0.5
""")

        summary = get_metrics_summary(workspace)

        # Should recommend reviewing Full-Stack CRUD
        assert any("Full-Stack CRUD" in rec for rec in summary["recommendations"])


class TestReunifyMetricsIntegration:
    """Tests for metrics integration with reunify."""

    def test_reunify_extracts_effort_on_success(self, tmp_path: Path) -> None:
        """Reunify should extract effort when all tasks succeed."""
        # This test documents expected behavior after wiring
        workspace = tmp_path / ".cleave"
        workspace.mkdir()

        # Create manifest with assessment data
        (workspace / "manifest.yaml").write_text("""
version: 1
root_directive: "Test directive"
intent:
  goal: "Test"
children:
  - id: 0
    label: "Task A"
""")

        # Create successful task
        (workspace / "0-task.md").write_text("""
---
task_id: 0
---
# Task 0

## Result

**Status:** SUCCESS

**Summary:** Straightforward implementation completed.

**Artifacts:**
- file.py
""")

        # Create metrics file with recorded assessment
        directive_hash = hash_directive("Test directive")
        (workspace / "metrics.yaml").write_text(f"""
assessments:
  - directive_hash: {directive_hash}
    timestamp: "2024-01-01T00:00:00"
    predicted_complexity: 3.0
    predicted_decision: execute
    pattern_matched: null
    pattern_confidence: 0.0
    systems: 2
    modifiers: []
    actual_effort: null
    actual_success: null
    accuracy_score: null
""")

        # Extract outcomes (this is what reunify should call)
        outcomes = extract_outcomes_from_tasks(workspace)
        assert len(outcomes) == 1
        assert outcomes[0]["inferred_effort"] == "low"


class TestMultiPatternProbeQuestions:
    """Tests for multi-pattern question merging."""

    def test_merges_questions_from_multiple_patterns(self, tmp_path: Path) -> None:
        """Should generate questions from all matching patterns."""
        from cleave.core.probe import probe_codebase

        # Directive that could match both authentication AND external_integration
        directive = "Add OAuth login with Stripe payment integration"

        result = probe_codebase(directive, tmp_path)

        # Should have questions from at least one pattern
        # After multi-pattern merging, should have from multiple
        assert len(result.triggered_questions) >= 1

    def test_deduplicates_identical_questions(self, tmp_path: Path) -> None:
        """Should not duplicate identical questions."""
        from cleave.core.probe import generate_probe_questions

        # This tests that if two patterns have the same "always" question,
        # it only appears once
        questions = generate_probe_questions(
            directive="Add authentication with JWT",
            root_dir=tmp_path,
            pattern_id=None,  # Check all patterns
        )

        # No duplicate questions
        question_texts = [q.question for q in questions]
        assert len(question_texts) == len(set(question_texts))
