"""Tests for pytest plugin."""

import os
import subprocess
import sys
from types import SimpleNamespace

import llmdebug.pytest_plugin as pytest_plugin_module
from llmdebug import get_latest_snapshot
from llmdebug.config import SnapshotConfig


def test_plugin_captures_on_failure(tmp_path):
    """Test that the pytest plugin captures snapshots on test failures."""
    # Create a test file
    test_file = tmp_path / "test_example.py"
    test_file.write_text(
        """
def test_failing():
    x = [1, 2, 3]
    assert False, "intentional failure"
"""
    )

    # Run pytest in subprocess
    result = subprocess.run(
        [sys.executable, "-m", "pytest", str(test_file), "-v"],
        cwd=tmp_path,
        capture_output=True,
        text=True,
    )

    # Test should fail
    assert result.returncode != 0

    # Check that snapshot was created
    llmdebug_dir = tmp_path / ".llmdebug"
    assert llmdebug_dir.exists(), f"No .llmdebug dir. stderr: {result.stderr}"

    snapshots = list(llmdebug_dir.glob("*.json"))
    assert len(snapshots) >= 1  # At least the snapshot (and maybe latest.json)

    # Check that repro command is present (use get_latest_snapshot to auto-expand keys)
    snapshot = get_latest_snapshot(str(llmdebug_dir))
    assert snapshot is not None, "Failed to read snapshot"
    repro = snapshot.get("pytest", {}).get("repro")
    assert isinstance(repro, list)
    assert "-m" in repro
    assert "pytest" in repro
    assert "test_example.py::test_failing" in repro


def test_no_snapshot_marker(tmp_path):
    """Test that no_snapshot marker prevents capture."""
    test_file = tmp_path / "test_no_capture.py"
    test_file.write_text(
        """
import pytest

@pytest.mark.no_snapshot
def test_failing_no_capture():
    assert False, "should not capture"
"""
    )

    result = subprocess.run(
        [sys.executable, "-m", "pytest", str(test_file), "-v"],
        cwd=tmp_path,
        capture_output=True,
        text=True,
    )

    # Test should fail
    assert result.returncode != 0

    # Check that no snapshot was created for this specific test
    llmdebug_dir = tmp_path / ".llmdebug"
    if llmdebug_dir.exists():
        snapshots = list(llmdebug_dir.glob("*test_failing_no_capture*.json"))
        assert len(snapshots) == 0, "Snapshot should not be created for no_snapshot marker"


def test_passing_test_no_snapshot(tmp_path):
    """Test that passing tests don't create snapshots."""
    test_file = tmp_path / "test_passing.py"
    test_file.write_text(
        """
def test_passing():
    assert True
"""
    )

    result = subprocess.run(
        [sys.executable, "-m", "pytest", str(test_file), "-v"],
        cwd=tmp_path,
        capture_output=True,
        text=True,
    )

    # Test should pass
    assert result.returncode == 0

    # Check that no snapshot was created
    llmdebug_dir = tmp_path / ".llmdebug"
    assert not llmdebug_dir.exists() or len(list(llmdebug_dir.glob("*.json"))) == 0


def _run_pytest_and_get_snapshot(tmp_path, test_code, env_overrides=None):
    """Helper: write test file, run pytest subprocess, return snapshot."""
    test_file = tmp_path / "test_subject.py"
    test_file.write_text(test_code)

    env = os.environ.copy()
    if env_overrides:
        env.update(env_overrides)

    result = subprocess.run(
        [sys.executable, "-m", "pytest", str(test_file), "-v"],
        cwd=tmp_path,
        capture_output=True,
        text=True,
        env=env,
    )
    assert result.returncode != 0, f"Test was expected to fail.\nstdout:\n{result.stdout}"

    llmdebug_dir = tmp_path / ".llmdebug"
    assert llmdebug_dir.exists(), f"No .llmdebug dir. stderr: {result.stderr}"

    snapshot = get_latest_snapshot(str(llmdebug_dir))
    assert snapshot is not None, "Failed to read snapshot"
    return snapshot


def test_fixture_values_in_snapshot(tmp_path):
    """Test that custom fixture values appear in the snapshot and internal fixtures are excluded."""
    snapshot = _run_pytest_and_get_snapshot(
        tmp_path,
        """
import pytest

@pytest.fixture
def data():
    return [1, 2, 3]

@pytest.fixture
def label():
    return "hello"

def test_with_fixtures(data, label, request):
    assert False, "intentional failure"
""",
    )
    pytest_ctx = snapshot["pytest"]
    fixtures = pytest_ctx.get("fixtures")
    assert fixtures is not None, f"Expected 'fixtures' key. pytest keys: {list(pytest_ctx.keys())}"
    assert "data" in fixtures
    assert "label" in fixtures
    # Internal fixture 'request' should be excluded
    assert "request" not in fixtures


def test_markers_in_snapshot(tmp_path):
    """Test that custom markers and timeout marker appear in the snapshot."""
    snapshot = _run_pytest_and_get_snapshot(
        tmp_path,
        """
import pytest

@pytest.mark.slow
@pytest.mark.timeout(30)
def test_marked():
    assert False, "intentional failure"
""",
    )
    pytest_ctx = snapshot["pytest"]
    markers = pytest_ctx.get("markers")
    assert markers is not None, f"Expected 'markers' key. pytest keys: {list(pytest_ctx.keys())}"
    assert "slow" in markers
    # timeout marker is captured separately in timeout_config, but also appears in markers
    assert "timeout" in markers

    # Timeout config should also be set
    toc = pytest_ctx.get("timeout_config")
    assert toc is not None, f"Expected 'timeout_config' key. pytest keys: {list(pytest_ctx.keys())}"
    assert toc["seconds"] == 30


def test_param_indices_in_snapshot(tmp_path):
    """Test that parametrize indices are captured."""
    snapshot = _run_pytest_and_get_snapshot(
        tmp_path,
        """
import pytest

@pytest.mark.parametrize("x", [10, 20, 30])
def test_param(x):
    if x == 20:
        assert False, "fails on 20"
""",
    )
    pytest_ctx = snapshot["pytest"]
    param_indices = pytest_ctx.get("param_indices")
    assert param_indices is not None, (
        f"Expected 'param_indices'. pytest keys: {list(pytest_ctx.keys())}"
    )
    assert "x" in param_indices
    # x=20 is index 1 in the parametrize list [10, 20, 30]
    assert param_indices["x"] == 1


def test_internal_fixtures_excluded(tmp_path):
    """Test that internal/noisy fixtures like capsys and tmp_path don't appear."""
    snapshot = _run_pytest_and_get_snapshot(
        tmp_path,
        """
def test_with_capsys(capsys, tmp_path):
    print("hello")
    assert False, "intentional failure"
""",
    )
    pytest_ctx = snapshot["pytest"]
    fixtures = pytest_ctx.get("fixtures", {})
    assert "capsys" not in fixtures
    # tmp_path is NOT in the exclude list and should appear
    assert "tmp_path" in fixtures


def test_fixtures_disabled_by_env(tmp_path):
    """Test that LLMDEBUG_INCLUDE_FIXTURES=false suppresses fixtures."""
    snapshot = _run_pytest_and_get_snapshot(
        tmp_path,
        """
import pytest

@pytest.fixture
def data():
    return [1, 2, 3]

def test_with_fixture(data):
    assert False, "intentional failure"
""",
        env_overrides={"LLMDEBUG_INCLUDE_FIXTURES": "false"},
    )
    pytest_ctx = snapshot["pytest"]
    assert "fixtures" not in pytest_ctx


def test_redaction_profile_from_env(tmp_path):
    """Test that redaction profile env applies to pytest snapshot capture."""
    snapshot = _run_pytest_and_get_snapshot(
        tmp_path,
        """
def test_secret_failure():
    assert False, "password=secret123"
""",
        env_overrides={"LLMDEBUG_REDACTION_PROFILE": "prod"},
    )
    message = snapshot["exception"]["message"]
    assert "[REDACTED]" in message
    assert "password=secret123" not in message


def test_default_redaction_profile_is_ci(monkeypatch):
    monkeypatch.delenv("LLMDEBUG_REDACTION_PROFILE", raising=False)

    cfg = pytest_plugin_module._build_snapshot_config_from_env(debug=False)

    assert cfg.redaction_profile == "ci"
    assert cfg.redact_exception_strings is True
    assert len(cfg.redact) > 0


def test_build_snapshot_config_reads_include_coverage_env(monkeypatch):
    monkeypatch.setenv("LLMDEBUG_INCLUDE_COVERAGE", "false")
    cfg_disabled = pytest_plugin_module._build_snapshot_config_from_env(debug=False)
    assert cfg_disabled.include_coverage is False

    monkeypatch.setenv("LLMDEBUG_INCLUDE_COVERAGE", "true")
    cfg_enabled = pytest_plugin_module._build_snapshot_config_from_env(debug=False)
    assert cfg_enabled.include_coverage is True


def test_pytest_context_redacts_captured_streams_by_default(tmp_path):
    """CI redaction defaults should redact sensitive stdout/stderr capture."""
    snapshot = _run_pytest_and_get_snapshot(
        tmp_path,
        """
import sys

def test_secret_in_output():
    print("api_key=secret123")
    print("auth_token=topsecret", file=sys.stderr)
    assert False, "boom"
""",
    )
    pytest_ctx = snapshot["pytest"]
    capstdout = pytest_ctx.get("capstdout", "")
    capstderr = pytest_ctx.get("capstderr", "")
    assert "[REDACTED]" in capstdout
    assert "[REDACTED]" in capstderr
    assert "api_key=secret123" not in capstdout
    assert "auth_token=topsecret" not in capstderr


class _DummyMarker:
    def __init__(
        self,
        name: str,
        args: tuple[object, ...] = (),
        kwargs: dict[str, object] | None = None,
    ):
        self.name = name
        self.args = args
        self.kwargs = kwargs or {}


class _DummyCallSpec:
    def __init__(self) -> None:
        self.params = {"x": 1}
        self.indices = {"x": 0}


class _DummyItem:
    def __init__(self, *, include_no_snapshot: bool = False) -> None:
        self.nodeid = "tests/test_subject.py::test_case"
        self.stash: dict[object, list[str]] = {}
        self.fixturenames = ["data", "request"]
        self.funcargs = {"data": [1, 2, 3], "request": "ignored"}
        self.callspec = _DummyCallSpec()
        self.config = SimpleNamespace(workerinput={"workerid": "gw0"})
        self._include_no_snapshot = include_no_snapshot

    def get_closest_marker(self, name: str):
        if name == "timeout":
            return _DummyMarker("timeout", args=(30,))
        if name == "no_snapshot" and self._include_no_snapshot:
            return _DummyMarker("no_snapshot")
        return None

    def iter_markers(self):
        return [
            _DummyMarker("slow"),
            _DummyMarker("timeout", args=(30,), kwargs={"method": "thread"}),
            _DummyMarker("parametrize"),
        ]


class _DummyReport:
    when = "call"
    failed = True
    outcome = "failed"
    nodeid = "tests/test_subject.py::test_case"
    longreprtext = "assert False"
    capstdout = "stdout text"
    capstderr = "stderr text"


class _DummyOutcome:
    def __init__(self, report) -> None:
        self._report = report

    def get_result(self):
        return self._report


class _DummyExcInfo:
    def __init__(self, exc: Exception) -> None:
        self.value = exc
        self.tb = exc.__traceback__


def _run_hook(item, call, report) -> None:
    hook = pytest_plugin_module.pytest_runtest_makereport(item, call)
    next(hook)
    try:
        hook.send(_DummyOutcome(report))
    except StopIteration:
        pass


def test_pytest_plugin_helpers_build_config_and_context(monkeypatch, capsys):
    monkeypatch.setenv("LLMDEBUG_INCLUDE_MODULES", "pkg.alpha, pkg.beta")
    monkeypatch.setenv("LLMDEBUG_REDACTION_PROFILE", "invalid-profile")
    monkeypatch.setenv("LLMDEBUG_OUTPUT_FORMAT", "toon")

    cfg = pytest_plugin_module._build_snapshot_config_from_env(debug=True)
    assert isinstance(cfg, SnapshotConfig)
    assert cfg.include_modules == ("pkg.alpha", "pkg.beta")
    assert cfg.output_format == "toon"

    stderr = capsys.readouterr().err
    assert "llmdebug:" in stderr
    assert pytest_plugin_module._parse_include_modules("a,b,, c ") == ("a", "b", "c")
    assert pytest_plugin_module._resolve_output_format("json") == "json"
    assert pytest_plugin_module._resolve_output_format("unknown") == "json_compact"

    item = _DummyItem()
    report = _DummyReport()
    pytest_context, callspec = pytest_plugin_module._build_base_pytest_context(item, report, cfg)
    assert pytest_context["nodeid"] == report.nodeid
    assert callspec is item.callspec

    pytest_plugin_module._capture_fixture_context(item, cfg, pytest_context)
    assert "fixtures" in pytest_context
    fixtures = pytest_context["fixtures"]
    assert isinstance(fixtures, dict)
    assert "request" not in fixtures

    pytest_plugin_module._capture_marker_context(item, cfg, pytest_context)
    assert "markers" in pytest_context
    markers = pytest_context["markers"]
    assert isinstance(markers, dict)
    assert "slow" in markers
    assert "parametrize" not in markers

    pytest_plugin_module._capture_timeout_context(item, cfg, pytest_context)
    timeout_config = pytest_context["timeout_config"]
    assert isinstance(timeout_config, dict)
    assert timeout_config["seconds"] == 30

    pytest_plugin_module._capture_param_indices(item.callspec, pytest_context)
    param_indices = pytest_context["param_indices"]
    assert isinstance(param_indices, dict)
    assert param_indices["x"] == 0

    pytest_plugin_module._capture_xdist_worker(item, pytest_context)
    assert pytest_context["xdist_worker"] == "gw0"

    item.stash[pytest_plugin_module._flaky_key] = ["failed", "passed"]
    pytest_plugin_module._capture_flaky_metadata(item, pytest_context)
    assert pytest_context["flaky"] is True

    warnings: list[str] = []
    pytest_plugin_module._capture_context_block(
        warnings,
        "boom",
        lambda: (_ for _ in ()).throw(RuntimeError("x")),
    )
    assert warnings and "boom" in warnings[0]


def test_pytest_plugin_hook_calls_capture_exception(monkeypatch):
    captured: dict[str, object] = {}

    def _fake_capture_exception(name, exc, tb, cfg, *, extra):
        captured["name"] = name
        captured["exc"] = exc
        captured["tb"] = tb
        captured["cfg"] = cfg
        captured["extra"] = extra

    monkeypatch.setattr(pytest_plugin_module, "capture_exception", _fake_capture_exception)
    monkeypatch.setattr(
        pytest_plugin_module,
        "_build_snapshot_config_from_env",
        lambda _debug: SnapshotConfig(output_format="json", include_coverage=True),
    )
    monkeypatch.setattr(
        pytest_plugin_module,
        "_capture_coverage_context",
        lambda *_args, **_kwargs: {"files": []},
    )

    item = _DummyItem()
    report = _DummyReport()
    try:
        raise RuntimeError("hook failure")
    except RuntimeError as exc:
        call = SimpleNamespace(excinfo=_DummyExcInfo(exc))

    _run_hook(item, call, report)

    assert captured["name"] == "tests_test_subject.py_test_case"
    extra = captured["extra"]
    assert isinstance(extra, dict)
    assert "pytest" in extra
    pytest_context = extra["pytest"]
    assert pytest_context["nodeid"] == report.nodeid
    assert "coverage" in pytest_context
    assert pytest_context["xdist_worker"] == "gw0"
    assert item.stash[pytest_plugin_module._flaky_key] == ["failed"]


def test_pytest_plugin_hook_skips_coverage_when_disabled(monkeypatch):
    captured: dict[str, object] = {}

    def _fake_capture_exception(name, exc, tb, cfg, *, extra):
        captured["name"] = name
        captured["extra"] = extra

    monkeypatch.setattr(pytest_plugin_module, "capture_exception", _fake_capture_exception)
    monkeypatch.setattr(
        pytest_plugin_module,
        "_build_snapshot_config_from_env",
        lambda _debug: SnapshotConfig(output_format="json", include_coverage=False),
    )
    monkeypatch.setattr(
        pytest_plugin_module,
        "_capture_coverage_context",
        lambda *_args, **_kwargs: (_ for _ in ()).throw(
            AssertionError("coverage extraction should be skipped")
        ),
    )

    item = _DummyItem()
    report = _DummyReport()
    try:
        raise RuntimeError("hook failure")
    except RuntimeError as exc:
        call = SimpleNamespace(excinfo=_DummyExcInfo(exc))

    _run_hook(item, call, report)

    extra = captured["extra"]
    assert isinstance(extra, dict)
    pytest_context = extra["pytest"]
    assert "coverage" not in pytest_context


def test_pytest_plugin_hook_handles_no_snapshot_and_missing_excinfo(monkeypatch):
    called = {"count": 0}
    monkeypatch.setattr(
        pytest_plugin_module,
        "capture_exception",
        lambda *_args, **_kwargs: called.__setitem__("count", called["count"] + 1),
    )
    monkeypatch.setattr(
        pytest_plugin_module,
        "_build_snapshot_config_from_env",
        lambda _debug: SnapshotConfig(output_format="json"),
    )

    report = _DummyReport()
    no_snap_item = _DummyItem(include_no_snapshot=True)
    _run_hook(no_snap_item, SimpleNamespace(excinfo=None), report)

    regular_item = _DummyItem()
    _run_hook(regular_item, SimpleNamespace(excinfo=None), report)
    assert called["count"] == 0


def test_pytest_plugin_hook_logs_capture_exception_failures(monkeypatch, capsys):
    monkeypatch.setattr(
        pytest_plugin_module,
        "capture_exception",
        lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("capture boom")),
    )
    monkeypatch.setattr(
        pytest_plugin_module,
        "_build_snapshot_config_from_env",
        lambda _debug: SnapshotConfig(debug=True, output_format="json"),
    )

    item = _DummyItem()
    report = _DummyReport()
    try:
        raise ValueError("hook failure")
    except ValueError as exc:
        call = SimpleNamespace(excinfo=_DummyExcInfo(exc))

    _run_hook(item, call, report)
    stderr = capsys.readouterr().err
    assert "capture failed for tests_test_subject.py_test_case" in stderr
