"""Additional edge-case tests for CLI helper branches."""

from __future__ import annotations

import os
from pathlib import Path
from types import SimpleNamespace
from typing import Any

import click
import pytest
from click.testing import CliRunner

from llmdebug import cli


class _FakeConsole:
    def __init__(self) -> None:
        self.items: list[Any] = []

    def print(self, item: Any = "", *args: Any, **kwargs: Any) -> None:
        self.items.append(item)


def test_stdout_is_tty_handles_noncallable_and_errors(monkeypatch: pytest.MonkeyPatch) -> None:
    class _NoIsatty:
        isatty = True

    class _BadIsatty:
        def isatty(self) -> bool:
            raise RuntimeError("boom")

    monkeypatch.setattr(click, "get_text_stream", lambda _name: _NoIsatty())
    assert cli._stdout_is_tty() is False

    monkeypatch.setattr(click, "get_text_stream", lambda _name: _BadIsatty())
    assert cli._stdout_is_tty() is False


def test_truncation_helpers_cover_boundary_conditions() -> None:
    assert cli._truncate_text("abcdef", 0) == ""
    assert cli._truncate_text("abcdef", 3) == "abc"
    assert cli._truncate_text("abcdef", 5) == "ab..."

    assert cli._truncate_tail("abcdef", 0) == ""
    assert cli._truncate_tail("abcdef", 3) == "def"
    assert cli._truncate_tail("abcdef", 5) == "...ef"


def test_get_console_returns_console_when_rich_and_tty(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(cli, "HAS_RICH", True)
    monkeypatch.setattr(cli, "_stdout_is_tty", lambda: True)
    monkeypatch.setitem(os.environ, "NO_COLOR", "1")
    assert cli._get_console() is not None


def test_snapshot_loader_wrappers_translate_to_system_exit(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setattr(
        cli,
        "_load_snapshot_impl",
        lambda *_a, **_k: (_ for _ in ()).throw(FileNotFoundError("missing snapshot")),
    )
    with pytest.raises(SystemExit, match="1"):
        cli._load_snapshot(None, ".llmdebug")

    monkeypatch.setattr(
        cli,
        "_load_snapshot_raw_impl",
        lambda *_a, **_k: (_ for _ in ()).throw(FileNotFoundError("missing raw snapshot")),
    )
    with pytest.raises(SystemExit, match="1"):
        cli._load_snapshot_raw(None, ".llmdebug")

    monkeypatch.setattr(
        cli,
        "_load_snapshot_from_file_impl",
        lambda *_a, **_k: (_ for _ in ()).throw(ImportError("toon missing")),
    )
    with pytest.raises(SystemExit, match="1"):
        cli._load_snapshot_from_file(Path("missing.json"))


def test_print_footer_plaintext_includes_git_and_repro(capsys: pytest.CaptureFixture[str]) -> None:
    snapshot = {
        "git": {"commit": "abc1234", "branch": "main", "dirty": True},
        "pytest": {"repro": ["/usr/bin/python3", "-m", "pytest", "tests/test_x.py::test_y", "-q"]},
    }
    cli._print_footer(snapshot, None)
    out = capsys.readouterr().out
    assert "git: abc1234 main *dirty*" in out
    assert "repro: python -m pytest tests/test_x.py::test_y -q" in out


def test_print_exception_panel_handles_malformed_exception(
    capsys: pytest.CaptureFixture[str],
) -> None:
    cli._print_exception_panel({"exception": "not-a-dict"}, None)
    out = capsys.readouterr().out
    assert "UnknownError" in out


def test_print_footer_handles_malformed_git_and_pytest(capsys: pytest.CaptureFixture[str]) -> None:
    cli._print_footer({"git": "bad", "pytest": "bad"}, None)
    out = capsys.readouterr().out
    assert out == ""


def test_print_frame_rich_with_and_without_source(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(cli, "HAS_RICH", True)
    console = _FakeConsole()

    with_source = {
        "file": "app.py",
        "line": 10,
        "function": "fn",
        "source": {"start": 10, "end": 10, "snippet": [{"lineno": 10, "code": "x = 1"}]},
        "locals": {"arr": {"__array__": "numpy.ndarray", "shape": [0, 2]}},
        "locals_meta": {"arr": {"type": "numpy.ndarray"}},
    }
    cli._print_frame_rich(with_source, 0, True, console)

    no_source = {"file": "app.py", "line": 11, "function": "fn2", "code": "return x", "locals": {}}
    cli._print_frame_rich(no_source, 1, False, console)

    assert len(console.items) >= 2


def test_print_frame_console_branch_with_locals_and_fallback(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setattr(cli, "HAS_RICH", True)
    console = _FakeConsole()
    frame = {
        "file": "app.py",
        "line": 42,
        "function": "run",
        "source": {"start": 41, "end": 43, "snippet": [{"lineno": 42, "code": "x = 1"}]},
        "locals": {"value": 1, "@py_internal": "skip"},
        "locals_meta": {"value": {"type": "builtins.int"}},
    }
    cli._print_frame(frame, 0, True, console, show_locals=True)

    frame_no_parts = {"file": "app.py", "line": 43, "function": "run", "code": "return x"}
    cli._print_frame(frame_no_parts, 1, False, console, show_locals=True)

    assert len(console.items) >= 2


def test_print_diff_rich_covers_change_kinds(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(cli, "HAS_RICH", True)
    console = _FakeConsole()

    changes = [
        SimpleNamespace(kind="modified", name="a", old_value="1", new_value="2"),
        SimpleNamespace(kind="added", name="b", old_value="", new_value="3"),
        SimpleNamespace(kind="removed", name="c", old_value="4", new_value=""),
        SimpleNamespace(
            kind="type_changed",
            name="d",
            old_value="1",
            new_value="'1'",
            old_type="int",
            new_type="str",
        ),
        SimpleNamespace(kind="shape_changed", name="arr", old_value=[1, 2], new_value=[0, 2]),
    ]
    result = SimpleNamespace(
        old_name="old",
        new_name="new",
        same_test=False,
        exception=SimpleNamespace(
            changed=True,
            old_type="ValueError",
            new_type="TypeError",
            old_message="old message",
            new_message="new message",
            old_category="value_error",
            new_category="type_error",
        ),
        location=SimpleNamespace(changed=True, old_location="a.py:1", new_location="b.py:2"),
        locals_diff=SimpleNamespace(changes=changes, unchanged_count=3),
        git_changed=True,
        old_git={"commit": "aaa"},
        new_git={"commit": "bbb"},
    )
    cli._print_diff_rich(result, console)

    unchanged_result = SimpleNamespace(
        old_name="same",
        new_name="same",
        same_test=True,
        exception=SimpleNamespace(
            changed=False,
            old_type="ValueError",
            new_type="ValueError",
            old_message="same message",
            new_message="same message",
            old_category=None,
            new_category=None,
        ),
        location=SimpleNamespace(changed=False, old_location="a.py:1", new_location="a.py:1"),
        locals_diff=SimpleNamespace(changes=[], unchanged_count=0),
        git_changed=False,
        old_git={},
        new_git={},
    )
    cli._print_diff_rich(unchanged_result, console)

    assert len(console.items) >= 5


def test_perform_deletions_counts_success_and_failures(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    ok_file = tmp_path / "ok.json"
    ok_file.write_text("{}", encoding="utf-8")
    missing = tmp_path / "missing.json"

    deleted, failed = cli._perform_deletions([ok_file, missing])
    assert deleted == 1
    assert failed == 1
    assert "could not delete" in capsys.readouterr().err


def test_list_command_rich_branch(monkeypatch: pytest.MonkeyPatch) -> None:
    fake_console = _FakeConsole()
    monkeypatch.setattr(cli, "_get_console", lambda: fake_console)
    monkeypatch.setattr(
        cli,
        "_find_all_snapshots_impl",
        lambda _out_dir, limit=10: [
            {
                "mtime": 1_700_000_000.0,
                "name": "test-name",
                "exc_type": "ValueError",
                "file": "app.py",
                "line": 42,
            }
        ],
    )

    runner = CliRunner(mix_stderr=False)
    result = runner.invoke(cli.main, ["list", "--dir", ".llmdebug"])
    assert result.exit_code == 0
    assert fake_console.items


def test_list_command_handles_malformed_entry_fields(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        cli,
        "_find_all_snapshots_impl",
        lambda _out_dir, limit=10: [
            {"mtime": "nan", "name": "bad-row"},
            "not-a-dict",
        ],
    )

    runner = CliRunner(mix_stderr=False)
    result = runner.invoke(cli.main, ["list", "--dir", ".llmdebug"])
    assert result.exit_code == 0
    assert "?" in result.output
