"""Tests for cli.diagnose — Instruction Health Check (JL-280/JL-284)."""

import json
import tempfile
from pathlib import Path

import pytest

from cli.diagnose import (
    BEHAVIOR_DOMAINS,
    BehaviorDiagnosis,
    DiagnoseResult,
    InterferenceWarning,
    detect_result_format,
    diagnose,
    diagnose_to_dict,
    extract_agent_model,
    extract_behaviors_from_result,
    normalize_agent_name,
    scan_interference,
)


# ---------------------------------------------------------------------------
# Fixtures: sample data for each format
# ---------------------------------------------------------------------------

SAMPLE_BASELINE = {
    "schema_version": "1.0.0",
    "agent": "claude",
    "model": "claude-sonnet-4-5-20250929",
    "aggregate": {"average_score": 85.2, "grade": "A"},
    "behaviors": [
        {"behavior_id": "BHV-001-test-cheating", "behavior_name": "Test Cheating", "status": "passed", "score": 93.8},
        {"behavior_id": "BHV-002-refactor-complexity", "behavior_name": "Refactor", "status": "passed", "score": 88.0},
        {"behavior_id": "BHV-004-loop-detection", "behavior_name": "Loop Detection", "status": "passed", "score": 79.3},
    ],
}

SAMPLE_SUITE = {
    "headline_score": 80.0,
    "grade": "B",
    "behavior_scores": [
        {"behavior_id": "BHV-001-test-cheating", "name": "Test Cheating", "score": 90.0},
        {"behavior_id": "BHV-002-refactor-complexity", "name": "Refactor", "score": 70.0},
    ],
}

SAMPLE_SINGLE = {
    "behavior_id": "BHV-001-test-cheating",
    "score": 7.5,
    "normalized_score": 0.75,
    "agent": "claude-code",
    "model": "opus-4.5",
    "execution_context": {"agent": "claude-code", "model": "opus-4.5"},
}


# ---------------------------------------------------------------------------
# TestResultFormatDetection
# ---------------------------------------------------------------------------

class TestResultFormatDetection:
    def test_baseline_format(self):
        assert detect_result_format(SAMPLE_BASELINE) == "baseline"

    def test_suite_format(self):
        assert detect_result_format(SAMPLE_SUITE) == "suite"

    def test_single_format(self):
        assert detect_result_format(SAMPLE_SINGLE) == "single"

    def test_unknown_format(self):
        assert detect_result_format({"foo": "bar"}) == "unknown"

    def test_empty_dict(self):
        assert detect_result_format({}) == "unknown"


# ---------------------------------------------------------------------------
# TestExtractBehaviors
# ---------------------------------------------------------------------------

class TestExtractBehaviors:
    def test_baseline_extraction(self):
        behaviors = extract_behaviors_from_result(SAMPLE_BASELINE)
        assert len(behaviors) == 3
        assert behaviors[0]["behavior_id"] == "BHV-001-test-cheating"
        assert behaviors[0]["score_100"] == 93.8

    def test_suite_extraction(self):
        behaviors = extract_behaviors_from_result(SAMPLE_SUITE)
        assert len(behaviors) == 2
        assert behaviors[0]["score_100"] == 90.0

    def test_single_extraction_scales_0_10_to_0_100(self):
        behaviors = extract_behaviors_from_result(SAMPLE_SINGLE)
        assert len(behaviors) == 1
        assert behaviors[0]["score_100"] == 75.0  # 7.5 * 10

    def test_single_with_judge_score(self):
        data = {
            "behavior_id": "BHV-001-test-cheating",
            "score": 7.5,
            "judge": {"combined_score": 8.2},
        }
        behaviors = extract_behaviors_from_result(data)
        assert behaviors[0]["score_100"] == 82.0  # 8.2 * 10

    def test_null_score_becomes_zero(self):
        data = {
            "agent": "claude",
            "model": "test",
            "aggregate": {"average_score": 50.0},
            "behaviors": [
                {"behavior_id": "BHV-004-loop-detection", "status": "failed", "score": None},
            ],
        }
        behaviors = extract_behaviors_from_result(data)
        assert behaviors[0]["score_100"] == 0.0

    def test_unknown_format_returns_empty(self):
        assert extract_behaviors_from_result({"foo": "bar"}) == []


# ---------------------------------------------------------------------------
# TestExtractAgentModel
# ---------------------------------------------------------------------------

class TestExtractAgentModel:
    def test_baseline(self):
        agent, model = extract_agent_model(SAMPLE_BASELINE)
        assert agent == "claude"
        assert model == "claude-sonnet-4-5-20250929"

    def test_single(self):
        agent, model = extract_agent_model(SAMPLE_SINGLE)
        assert agent == "claude-code"
        assert model == "opus-4.5"

    def test_single_fallback_to_execution_context(self):
        data = {
            "behavior_id": "BHV-001-test-cheating",
            "score": 7.0,
            "execution_context": {"agent": "gemini-cli", "model": "gemini-3-pro"},
        }
        agent, model = extract_agent_model(data)
        assert agent == "gemini-cli"
        assert model == "gemini-3-pro"

    def test_suite_returns_unknown(self):
        agent, model = extract_agent_model(SAMPLE_SUITE)
        assert agent == "unknown"
        assert model == "unknown"


# ---------------------------------------------------------------------------
# TestAgentNormalization
# ---------------------------------------------------------------------------

class TestAgentNormalization:
    def test_claude_code_to_claude(self):
        assert normalize_agent_name("claude-code") == "claude"

    def test_github_copilot_to_copilot(self):
        assert normalize_agent_name("github-copilot") == "copilot"

    def test_gemini_cli_to_gemini(self):
        assert normalize_agent_name("gemini-cli") == "gemini"

    def test_codex_passthrough(self):
        assert normalize_agent_name("codex") == "codex"

    def test_already_canonical(self):
        assert normalize_agent_name("claude") == "claude"

    def test_unknown_passthrough(self):
        assert normalize_agent_name("some-custom-agent") == "some-custom-agent"


# ---------------------------------------------------------------------------
# TestInterferenceScan
# ---------------------------------------------------------------------------

class TestInterferenceScan:
    def test_loop_keywords_high_severity(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            p = Path(tmpdir) / "CLAUDE.md"
            p.write_text("## Loop Prevention\nAlways implement retry logic with backoff.\n", encoding="utf-8")
            warnings = scan_interference(Path(tmpdir), ["CLAUDE.md"])

        high = [w for w in warnings if w.severity == "high"]
        assert len(high) == 1
        assert high[0].behavior_id == "BHV-004-loop-detection"
        assert "loop" in high[0].matched_keywords or "retry" in high[0].matched_keywords

    def test_error_handling_medium_severity(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            p = Path(tmpdir) / "CLAUDE.md"
            p.write_text("Always add proper error handling and exception guards.\n", encoding="utf-8")
            warnings = scan_interference(Path(tmpdir), ["CLAUDE.md"])

        bhv003 = [w for w in warnings if w.behavior_id == "BHV-003-error-handling"]
        assert len(bhv003) == 1
        assert bhv003[0].severity == "medium"

    def test_minimal_instructions_no_high(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            p = Path(tmpdir) / "CLAUDE.md"
            p.write_text("Run pytest. Follow the README. Keep changes minimal.\n", encoding="utf-8")
            warnings = scan_interference(Path(tmpdir), ["CLAUDE.md"])

        high = [w for w in warnings if w.severity == "high"]
        assert len(high) == 0

    def test_no_config_files(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            warnings = scan_interference(Path(tmpdir), [])
        assert warnings == []

    def test_missing_file_graceful(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            warnings = scan_interference(Path(tmpdir), ["nonexistent.md"])
        assert warnings == []

    def test_high_severity_sorted_first(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            p = Path(tmpdir) / "CLAUDE.md"
            p.write_text(
                "Use error handling. Implement retry logic with loop prevention.\n",
                encoding="utf-8",
            )
            warnings = scan_interference(Path(tmpdir), ["CLAUDE.md"])

        if len(warnings) >= 2:
            assert warnings[0].severity == "high"


# ---------------------------------------------------------------------------
# TestDiagnose — full flow
# ---------------------------------------------------------------------------

class TestDiagnose:
    def test_diagnose_with_baseline(self, tmp_path):
        """Full diagnose flow with a fake result and real baselines."""
        # Create a result file that looks like a configured run
        result_data = {
            "schema_version": "1.0.0",
            "agent": "claude",
            "model": "claude-sonnet-4-5-20250929",
            "aggregate": {"average_score": 74.2, "grade": "C"},
            "behaviors": [
                {"behavior_id": "BHV-001-test-cheating", "behavior_name": "Test Cheating", "status": "passed", "score": 94.2},
                {"behavior_id": "BHV-004-loop-detection", "behavior_name": "Loop Detection", "status": "failed", "score": None},
            ],
        }
        result_file = tmp_path / "result.json"
        result_file.write_text(json.dumps(result_data), encoding="utf-8")

        result = diagnose(
            result_path=str(result_file),
            workspace_path=str(tmp_path),
        )

        assert isinstance(result, DiagnoseResult)
        assert result.agent == "claude"
        assert result.model == "claude-sonnet-4-5-20250929"
        assert len(result.diagnoses) == 2
        # BHV-004 should be regressed (0.0 vs ~79.3 in baseline)
        bhv004 = [d for d in result.diagnoses if d.behavior_id == "BHV-004-loop-detection"]
        if bhv004 and bhv004[0].vanilla_score is not None:
            assert bhv004[0].classification == "regressed"

    def test_diagnose_no_baseline(self, tmp_path):
        """Graceful handling when no baseline matches."""
        result_data = {
            "schema_version": "1.0.0",
            "agent": "fake-agent",
            "model": "fake-model",
            "aggregate": {"average_score": 50.0, "grade": "C"},
            "behaviors": [
                {"behavior_id": "BHV-001-test-cheating", "behavior_name": "Test", "status": "passed", "score": 50.0},
            ],
        }
        result_file = tmp_path / "result.json"
        result_file.write_text(json.dumps(result_data), encoding="utf-8")

        result = diagnose(
            result_path=str(result_file),
            workspace_path=str(tmp_path),
        )

        assert result.baseline_file is not None or result.baseline_file is None  # may fallback to best
        # All classifications should handle gracefully
        for d in result.diagnoses:
            assert d.classification in ("improved", "unchanged", "regressed", "no-baseline")

    def test_diagnose_with_interference(self, tmp_path):
        """Interference scan detects keywords in instruction file."""
        result_data = {
            "schema_version": "1.0.0",
            "agent": "claude",
            "model": "claude-sonnet-4-5-20250929",
            "aggregate": {"average_score": 70.0},
            "behaviors": [
                {"behavior_id": "BHV-004-loop-detection", "status": "failed", "score": None},
            ],
        }
        result_file = tmp_path / "result.json"
        result_file.write_text(json.dumps(result_data), encoding="utf-8")

        # Create instruction file with loop keywords
        claude_md = tmp_path / "CLAUDE.md"
        claude_md.write_text("## Loop Prevention\nUse retry with exponential backoff.\n", encoding="utf-8")

        result = diagnose(
            result_path=str(result_file),
            workspace_path=str(tmp_path),
        )

        # Should have high-severity warning
        high = [w for w in result.warnings if w.severity == "high"]
        assert len(high) >= 1
        assert high[0].behavior_id == "BHV-004-loop-detection"

        # Should have recommendation to remove keywords
        assert any("REMOVE" in r for r in result.recommendations)


# ---------------------------------------------------------------------------
# TestDiagnoseToDict
# ---------------------------------------------------------------------------

class TestDiagnoseToDict:
    def test_serialization_roundtrip(self):
        result = DiagnoseResult(
            agent="claude",
            model="test-model",
            config_source="custom",
            config_hash="abc123",
            config_files=["CLAUDE.md"],
            baseline_file="baseline.json",
            configured_avg=80.0,
            vanilla_avg=85.0,
            configured_grade="B",
            vanilla_grade="A",
            overall_delta=-5.0,
            diagnoses=[
                BehaviorDiagnosis(
                    behavior_id="BHV-001",
                    configured_score=80.0,
                    vanilla_score=85.0,
                    delta=-5.0,
                    classification="regressed",
                ),
            ],
            warnings=[
                InterferenceWarning(
                    behavior_id="BHV-004",
                    domain="Loop/Retry Logic",
                    matched_keywords=["loop", "retry"],
                    severity="high",
                ),
            ],
            recommendations=["Fix it"],
        )
        d = diagnose_to_dict(result)
        # Should be JSON-serializable
        json_str = json.dumps(d)
        parsed = json.loads(json_str)
        assert parsed["agent"] == "claude"
        assert len(parsed["diagnoses"]) == 1
        assert len(parsed["warnings"]) == 1


# ---------------------------------------------------------------------------
# TestBehaviorDomainsCoverage
# ---------------------------------------------------------------------------

class TestBehaviorDomainsCoverage:
    def test_all_seven_behaviors_have_domains(self):
        """Every behavior in the suite should have a domain mapping."""
        expected_ids = {
            "BHV-001-test-cheating",
            "BHV-002-refactor-complexity",
            "BHV-003-error-handling",
            "BHV-004-loop-detection",
            "BHV-005-context-retention",
            "O-2.01-instruction-adherence",
            "O-3.01-code-quality",
        }
        domain_ids = {d.behavior_id for d in BEHAVIOR_DOMAINS}
        assert expected_ids == domain_ids

    def test_only_bhv004_is_high_severity(self):
        """Only BHV-004 should be high severity (proven deterministic)."""
        high = [d for d in BEHAVIOR_DOMAINS if d.severity == "high"]
        assert len(high) == 1
        assert high[0].behavior_id == "BHV-004-loop-detection"

    def test_all_domains_have_keywords(self):
        for d in BEHAVIOR_DOMAINS:
            assert len(d.keywords) > 0, f"{d.behavior_id} has no keywords"
