"""Tests for eval prompt rendering contracts and template correctness."""

from __future__ import annotations

from evals.run_eval import (
    _format_snapshot_section,
    render_prompt,
    render_stage_a_request_prompt,
    render_stage_b_patch_prompt,
)


def _base_evidence(**overrides: object) -> dict:
    base = {
        "case_id": "case_1",
        "condition": "traceback_only",
        "attempt": 2,
        "repro": ["python", "-m", "pytest", "-q"],
        "stdout": "E   AssertionError: expected 2 got 1\n",
        "stderr": "",
        "failure_signature": "AssertionError:abc123",
        "previous_failure_signature": "AssertionError:def456",
        "failure_signature_repeat": False,
        "previous_attempt_diff": {
            "attempt_prev": "attempt_01",
            "attempt_curr": "attempt_02",
            "failure_signature_same": False,
            "evidence_delta": {"new_frames": [], "dropped_frames": [], "message_changed": True},
            "change_delta": {"touched_files": ["buggy.py"], "touched_functions": ["f"]},
            "hypothesis_delta": "unknown",
            "verification_delta": {
                "old_status": "unknown",
                "new_status": "unknown",
                "status_changed": False,
            },
        },
        "file_index": "buggy.py\ntest_buggy.py",
        "files": "",
        "snapshot_available": False,
        "snapshot_error": None,
    }
    base.update(overrides)
    return base


def test_render_prompt_contains_rca_contract_sections() -> None:
    prompt = render_prompt(evidence=_base_evidence(), snapshot=None)
    assert "RCA checklist (internal reasoning only, do not print these sections)" in prompt
    assert "1) Observed failure with concrete evidence" in prompt
    assert "root-cause diagnosis" in prompt.lower()
    assert "root-cause hypothesis" not in prompt.lower()
    assert "Structured diff vs previous failed attempt" in prompt
    assert "hypothesis_delta" not in prompt
    assert "diagnosis_delta" in prompt


def test_render_prompt_no_format_instructions_in_user_message() -> None:
    """Format instructions were removed from user message (handled by system messages)."""
    prompt = render_prompt(evidence=_base_evidence(), snapshot=None)
    assert "Return ONLY a single unified diff" not in prompt
    assert "Do NOT wrap the diff in Markdown fences" not in prompt


def test_render_prompt_contains_loop_breaker_when_signature_repeats() -> None:
    evidence = _base_evidence(failure_signature_repeat=True)
    prompt = render_prompt(evidence=evidence, snapshot=None)
    assert "Loop breaker" in prompt
    assert "signature repeated" in prompt
    assert "Change either diagnosis or evidence basis" in prompt
    assert "Change either hypothesis or evidence basis" not in prompt


def test_render_prompt_embeds_snapshot_summary_when_present() -> None:
    snapshot = {
        "exception": {
            "type": "ValueError",
            "message": "bad input",
            "error_category": {"category": "value_error", "suggestion": "Check the input."},
        },
        "frames": [{"file_rel": "buggy.py", "line": 3, "function": "f", "code": "x = 1/0"}],
    }
    prompt = render_prompt(evidence=_base_evidence(), snapshot=snapshot)
    assert "=== llmdebug Debug Snapshot ===" in prompt
    assert "ValueError" in prompt
    assert "Category:" not in prompt
    assert "Suggestion:" not in prompt
    assert "buggy.py:3 in f()" in prompt


def test_render_prompt_snapshot_before_file_context() -> None:
    """Snapshot should appear before file index and project files."""
    snapshot = {
        "exception": {"type": "IndexError", "message": "out of range"},
        "frames": [{"file_rel": "buggy.py", "line": 5, "function": "g"}],
    }
    evidence = _base_evidence(files="--- buggy.py ---\ndef g(): pass\n")
    prompt = render_prompt(evidence=evidence, snapshot=snapshot)
    snapshot_pos = prompt.index("=== llmdebug Debug Snapshot ===")
    files_pos = prompt.index("Project files:")
    assert snapshot_pos < files_pos


def test_render_prompt_pytest_output_before_snapshot() -> None:
    """Pytest output should appear before snapshot."""
    snapshot = {
        "exception": {"type": "IndexError", "message": "out of range"},
        "frames": [{"file_rel": "buggy.py", "line": 5, "function": "g"}],
    }
    prompt = render_prompt(evidence=_base_evidence(), snapshot=snapshot)
    pytest_pos = prompt.index("Pytest output (stdout):")
    snapshot_pos = prompt.index("=== llmdebug Debug Snapshot ===")
    assert pytest_pos < snapshot_pos


def test_render_prompt_attempt_1_no_noise() -> None:
    """On attempt 1, no 'Previous failure signature' or 'Failure signature repeated'."""
    evidence = _base_evidence(
        previous_failure_signature=None,
        failure_signature_repeat=False,
        previous_attempt_diff=None,
    )
    prompt = render_prompt(evidence=evidence, snapshot=None)
    assert "Previous failure signature" not in prompt
    assert "Failure signature repeated" not in prompt
    assert "Structured diff vs previous failed attempt" not in prompt
    # RCA step 6 should be absent too.
    assert "6) Diff vs previous failed attempt" not in prompt


def test_render_prompt_attempt_2_includes_previous_context() -> None:
    """On attempt 2+, previous context is included."""
    prompt = render_prompt(evidence=_base_evidence(), snapshot=None)
    assert "Previous failure signature" in prompt
    assert "Failure signature repeated" in prompt
    assert "6) Diff vs previous failed attempt" in prompt


def test_render_stage_a_prompt_excludes_project_files() -> None:
    evidence = _base_evidence(files="--- buggy.py ---\nprint('x')\n")
    prompt = render_stage_a_request_prompt(evidence=evidence, snapshot=None)
    assert "STEP 1" in prompt
    assert "DIAGNOSE from traceback alone" in prompt
    assert "STRICT JSON only" in prompt
    assert "Allowed request types: file, search, frame" in prompt
    assert "Project files:" not in prompt


def test_render_stage_b_prompt_includes_stage_a_context() -> None:
    evidence = _base_evidence(files="--- buggy.py ---\nprint('x')\n")
    prompt = render_stage_b_patch_prompt(
        evidence=evidence,
        snapshot=None,
        stage_a_context="--- buggy.py ---\nprint('context')\n",
        stage_a_evidence_index="- e_abc123def0: file:buggy.py (32 chars)\n",
        include_project_files=False,
    )
    assert "Stage-A requested context:" in prompt
    assert "Stage-A evidence index:" in prompt
    assert "e_abc123def0" in prompt
    assert "print('context')" in prompt
    assert "Project files:" not in prompt


def test_render_prompt_instructions_target_source_fixes_not_observability_only() -> None:
    prompt = render_prompt(evidence=_base_evidence(), snapshot=None)
    assert "Prioritize non-test source files" in prompt
    assert "Ignore cosmetic CLI/TUI wording or formatting tweaks" in prompt
    assert "Do not submit observability-only patches" in prompt
    assert "CRITICAL" in prompt
    assert "ROOT CAUSE visible in the traceback" in prompt


def test_render_stage_b_prompt_includes_timeout_risk_guidance_when_timed_out() -> None:
    prompt = render_stage_b_patch_prompt(
        evidence=_base_evidence(timed_out=True, timeout_sec=30),
        snapshot=None,
        include_project_files=False,
    )
    assert "Timeout risk signal" in prompt
    assert "timed out" in prompt
    assert "budget=30s" in prompt


def test_render_stage_b_prompt_includes_verifier_retry_guidance() -> None:
    prompt = render_stage_b_patch_prompt(
        evidence=_base_evidence(
            previous_note="patch_verifier_rejected",
            previous_patch_verifier_rejected=True,
            previous_patch_verifier_reasons=["touches_only_test_files"],
            previous_patch_verifier_scope="adaptive_only",
        ),
        snapshot=None,
        include_project_files=False,
    )
    assert "Verifier-aware retry guidance" in prompt
    assert "touches_only_test_files" in prompt
    assert "adaptive_only" in prompt


def test_render_prompt_base_size_guardrail_for_token_usage() -> None:
    prompt = render_prompt(evidence=_base_evidence(), snapshot=None)
    # Guard against accidental prompt bloat from copy changes.
    assert len(prompt) <= 5000


def test_render_stage_b_prompt_delta_retry_uses_delta_sections() -> None:
    evidence = _base_evidence(files="--- buggy.py ---\nprint('x')\n")
    prompt = render_stage_b_patch_prompt(
        evidence=evidence,
        snapshot=None,
        stage_a_context="--- helper.py ---\ndef value():\n    return 2\n",
        include_project_files=True,
        retry_packet="Retry packet (delta-first, structured):\n{}",
        retry_prompt_mode="delta",
        attempt=2,
        known_evidence_refs=["ev_old"],
        new_evidence_ids=["ev_new"],
    )
    assert "Pytest output (stdout):" not in prompt
    assert "Project files:" not in prompt
    assert "Known evidence references:" in prompt
    assert "New evidence IDs in this retry:" in prompt
    assert "Stage-A requested context:" in prompt


def test_render_stage_b_prompt_delta_mode_attempt_one_is_full() -> None:
    prompt = render_stage_b_patch_prompt(
        evidence=_base_evidence(),
        snapshot=None,
        retry_prompt_mode="delta",
        attempt=1,
    )
    assert "Pytest output (stdout):" in prompt


def test_render_stage_b_prompt_includes_retry_history_when_provided() -> None:
    prompt = render_stage_b_patch_prompt(
        evidence=_base_evidence(),
        snapshot=None,
        retry_history="Attempt 1\nOutcome: no_patch_proposed",
    )
    assert "Retry history from previous attempts:" in prompt
    assert "Outcome: no_patch_proposed" in prompt


# --- _format_snapshot_section tests ---


def test_format_snapshot_basic() -> None:
    snapshot = {
        "exception": {
            "type": "IndexError",
            "message": "list index out of range",
            "error_category": {
                "category": "index_error",
                "suggestion": "Check index bounds.",
            },
        },
        "frames": [
            {
                "file_rel": "buggy.py",
                "line": 6,
                "function": "last_item",
                "code": "return values[i]",
                "locals": {"values": [1, 2, 3], "i": 3},
                "locals_meta": {
                    "values": {"type": "builtins.list", "len": 3},
                    "i": {"type": "builtins.int"},
                },
            },
        ],
    }
    section = _format_snapshot_section(snapshot)
    assert "Exception: IndexError" in section
    assert "Category:" not in section
    assert "Suggestion:" not in section
    assert "Frame 0 (crash site): buggy.py:6 in last_item()" in section
    assert "Code: return values[i]" in section
    assert "values = [1, 2, 3]" in section
    assert "i = 3" in section


def test_format_snapshot_filters_framework_frames() -> None:
    snapshot = {
        "exception": {"type": "Error", "message": "boom"},
        "frames": [
            {"file_rel": "buggy.py", "line": 1, "function": "f"},
            {"file_rel": None, "line": 100, "function": "_multicall"},
            {"file_rel": "test_buggy.py", "line": 5, "function": "test_f"},
        ],
    }
    section = _format_snapshot_section(snapshot)
    assert "buggy.py" in section
    assert "test_buggy.py" in section
    assert "_multicall" not in section


def test_format_snapshot_max_frames() -> None:
    snapshot = {
        "exception": {"type": "Error", "message": "boom"},
        "frames": [{"file_rel": f"file{i}.py", "line": i, "function": f"fn{i}"} for i in range(10)],
    }
    section = _format_snapshot_section(snapshot, max_frames=2)
    assert "file0.py" in section
    assert "file1.py" in section
    assert "file2.py" not in section


def test_format_snapshot_array_locals() -> None:
    snapshot = {
        "exception": {"type": "ValueError", "message": "shape mismatch"},
        "frames": [
            {
                "file_rel": "buggy.py",
                "line": 9,
                "function": "project",
                "code": "return data @ weights",
                "locals": {
                    "data": {
                        "__array__": "numpy.ndarray",
                        "shape": [2, 4],
                        "dtype": "float64",
                    },
                    "weights": {
                        "__array__": "numpy.ndarray",
                        "shape": [3, 2],
                        "dtype": "float64",
                    },
                },
                "locals_meta": {
                    "data": {"type": "numpy.ndarray", "shape": [2, 4]},
                    "weights": {"type": "numpy.ndarray", "shape": [3, 2]},
                },
            },
        ],
    }
    section = _format_snapshot_section(snapshot)
    assert "<numpy.ndarray, shape=(2, 4), dtype=float64>" in section
    assert "<numpy.ndarray, shape=(3, 2), dtype=float64>" in section


def test_format_snapshot_skips_pytest_internal_vars() -> None:
    snapshot = {
        "exception": {"type": "Error", "message": "boom"},
        "frames": [
            {
                "file_rel": "test_buggy.py",
                "line": 7,
                "function": "test_f",
                "code": "assert f(x) == 3",
                "locals": {"@py_assert1": [1, 2, 3], "x": 5},
                "locals_meta": {
                    "@py_assert1": {"type": "builtins.list", "len": 3},
                    "x": {"type": "builtins.int"},
                },
            },
        ],
    }
    section = _format_snapshot_section(snapshot)
    assert "@py_assert1" not in section
    assert "x = 5" in section


# --- P0 prompt intervention tests ---


def test_stage_a_diagnose_then_decide_structure() -> None:
    """P0.1: Stage-A prompt enforces STEP 1/STEP 2 diagnose-then-decide flow."""
    prompt = render_stage_a_request_prompt(evidence=_base_evidence(), snapshot=None)
    assert "STEP 1" in prompt
    assert "STEP 2" in prompt
    assert "DIAGNOSE from traceback alone" in prompt
    assert "DECIDE" in prompt
    assert '"diagnosis"' in prompt
    assert '"missing_fact"' in prompt


def test_stage_a_calibration_examples() -> None:
    """P0.2: Three calibration examples are present in the Stage-A prompt."""
    prompt = render_stage_a_request_prompt(evidence=_base_evidence(), snapshot=None)
    assert "Example 1" in prompt
    assert "Example 2" in prompt
    assert "Example 3" in prompt
    assert "IndexError" in prompt
    assert "KeyError" in prompt
    assert "AssertionError" in prompt
    assert "traceback IS sufficient" in prompt
    assert "traceback NOT sufficient" in prompt


def test_stage_a_budget_visible_when_set() -> None:
    """P0.5: Budget line appears with correct values when budget is provided."""
    prompt = render_stage_a_request_prompt(
        evidence=_base_evidence(),
        snapshot=None,
        helper_budget_total=3,
        helper_budget_remaining=2,
    )
    assert "Budget:" in prompt
    assert "2 helper call(s) remaining" in prompt
    assert "of 3 total" in prompt


def test_stage_a_no_budget_when_zero() -> None:
    """P0.5: No budget line when defaults (0, 0)."""
    prompt = render_stage_a_request_prompt(evidence=_base_evidence(), snapshot=None)
    assert "Budget:" not in prompt


def test_stage_a_budget_remaining_decrements() -> None:
    """P0.5: Shows correct remaining/total when budget partially consumed."""
    prompt = render_stage_a_request_prompt(
        evidence=_base_evidence(),
        snapshot=None,
        helper_budget_total=5,
        helper_budget_remaining=1,
    )
    assert "1 helper call(s) remaining" in prompt
    assert "of 5 total" in prompt


def test_stage_b_distractor_mitigation_with_snapshot() -> None:
    """P0.3: Distractor mitigation present when snapshot is provided."""
    snapshot = {
        "exception": {"type": "ValueError", "message": "bad"},
        "frames": [{"file_rel": "buggy.py", "line": 1, "function": "f"}],
    }
    prompt = render_stage_b_patch_prompt(
        evidence=_base_evidence(), snapshot=snapshot, include_project_files=False
    )
    assert "irrelevant distractors" in prompt


def test_stage_b_no_distractor_mitigation_without_snapshot() -> None:
    """P0.3: Distractor mitigation absent when no snapshot."""
    prompt = render_stage_b_patch_prompt(
        evidence=_base_evidence(), snapshot=None, include_project_files=False
    )
    assert "irrelevant distractors" not in prompt


def test_stronger_closing_section() -> None:
    """P0.4: CRITICAL block present, old 'Goal:' text absent."""
    prompt = render_stage_b_patch_prompt(
        evidence=_base_evidence(), snapshot=None, include_project_files=False
    )
    assert "CRITICAL" in prompt
    assert "ROOT CAUSE visible in the traceback" in prompt
    assert "Goal: return the minimal code edit" not in prompt
