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 hypothesis" in prompt.lower()
    assert "Structured diff vs previous failed attempt" 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


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 "value_error" in prompt
    assert "Check the input." 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 "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",
        include_project_files=False,
    )
    assert "Stage-A requested context:" in prompt
    assert "print('context')" in prompt
    assert "Project files:" not in prompt


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: index_error" in section
    assert "Suggestion: Check index bounds." 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
