"""CLI tests for pulka compare."""

from __future__ import annotations

from pathlib import Path

import polars as pl
import pytest

from pulka.cli import main


def _write_parquet(path: Path, data: dict[str, list]) -> Path:
    pl.DataFrame(data).write_parquet(path)
    return path


def test_compare_identical_outputs_unchanged_rows(tmp_path: Path, capsys) -> None:
    left = _write_parquet(tmp_path / "left.parquet", {"a": [1, 2], "b": ["x", "y"]})
    right = _write_parquet(tmp_path / "right.parquet", {"a": [1, 2], "b": ["x", "y"]})
    out_path = tmp_path / "diff.parquet"

    exit_code = main(["compare", str(left), str(right), "--out", str(out_path)])
    captured = capsys.readouterr()

    assert exit_code == 0
    assert out_path.exists()
    diff_df = pl.read_parquet(out_path)
    assert diff_df.height == 2
    assert diff_df["global__kind"].to_list() == ["unchanged", "unchanged"]
    assert "Compare summary" in captured.out
    assert "Matched unchanged: 2" in captured.out
    assert f"Wrote {out_path}" in captured.out


def test_compare_changed_rows_sets_exit_code(tmp_path: Path, capsys) -> None:
    left = _write_parquet(tmp_path / "left.parquet", {"a": [1], "b": ["x"]})
    right = _write_parquet(tmp_path / "right.parquet", {"a": [2], "b": ["x"]})
    out_path = tmp_path / "diff.parquet"

    exit_code = main(["compare", str(left), str(right), "--out", str(out_path)])
    captured = capsys.readouterr()

    assert exit_code == 1
    assert out_path.exists()
    diff_df = pl.read_parquet(out_path)
    assert diff_df["global__kind"].to_list() == ["changed"]
    assert diff_df["a__kind"].to_list() == ["changed"]
    assert diff_df["b__kind"].to_list() == ["unchanged"]
    assert "changed" in captured.out


def test_compare_left_only_rows(tmp_path: Path) -> None:
    left = _write_parquet(tmp_path / "left.parquet", {"a": [1, 2], "b": ["x", "y"]})
    right = _write_parquet(tmp_path / "right.parquet", {"a": [1], "b": ["x"]})
    out_path = tmp_path / "diff.parquet"

    exit_code = main(["compare", str(left), str(right), "--out", str(out_path)])

    assert exit_code == 1
    diff_df = pl.read_parquet(out_path)
    assert set(diff_df["global__kind"].to_list()) == {"unchanged", "left_only"}
    left_only_df = diff_df.filter(pl.col("global__kind") == "left_only")
    assert left_only_df.height == 1
    assert left_only_df["a__kind"].to_list() == ["left_only"]
    assert left_only_df["b__kind"].to_list() == ["left_only"]


def test_diff_alias_runs_compare(tmp_path: Path) -> None:
    left = _write_parquet(tmp_path / "left.parquet", {"a": [1], "b": ["x"]})
    right = _write_parquet(tmp_path / "right.parquet", {"a": [2], "b": ["x"]})
    out_path = tmp_path / "diff.parquet"

    exit_code = main(["diff", str(left), str(right), "--out", str(out_path)])

    assert exit_code == 1
    diff_df = pl.read_parquet(out_path)
    assert diff_df["global__kind"].to_list() == ["changed"]
    assert diff_df["a__kind"].to_list() == ["changed"]
    assert diff_df["b__kind"].to_list() == ["unchanged"]


def test_compare_tui_launches_viewer(monkeypatch, tmp_path: Path) -> None:
    left = _write_parquet(tmp_path / "left.parquet", {"a": [1], "b": ["x"]})
    right = _write_parquet(tmp_path / "right.parquet", {"a": [1], "b": ["x"]})

    called: dict[str, object] = {}

    def _run_tui_app(viewer, recorder=None, on_shutdown=None):  # type: ignore[override]
        called["viewer"] = viewer
        called["recorder"] = recorder

    monkeypatch.setattr("pulka.tui.app.run_tui_app", _run_tui_app)
    monkeypatch.setattr("sys.stdin.isatty", lambda: True)
    monkeypatch.setattr("pulka.cli.compare._prompt_yes_no", lambda _prompt: True)

    exit_code = main(["compare", str(left), str(right)])

    assert exit_code == 0
    assert called


def test_compare_duplicate_keys_error(tmp_path: Path, capsys) -> None:
    left = _write_parquet(tmp_path / "left.parquet", {"id": [1, 1], "val": ["a", "b"]})
    right = _write_parquet(tmp_path / "right.parquet", {"id": [1, 2], "val": ["a", "b"]})
    out_path = tmp_path / "diff.parquet"

    with pytest.raises(SystemExit) as exc:
        main(["compare", str(left), str(right), "--keys", "id", "--out", str(out_path)])
    captured = capsys.readouterr()

    assert exc.value.code == 2
    assert "Duplicate keys" in captured.err


def test_compare_dtype_mismatch_error(tmp_path: Path, capsys) -> None:
    left = _write_parquet(tmp_path / "left.parquet", {"id": [1], "val": [1]})
    right = _write_parquet(tmp_path / "right.parquet", {"id": [1], "val": ["1"]})

    with pytest.raises(SystemExit) as exc:
        main(["compare", str(left), str(right)])
    captured = capsys.readouterr()

    assert exc.value.code == 2
    assert "dtype mismatch" in captured.err


def test_compare_no_common_columns_error(tmp_path: Path, capsys) -> None:
    left = _write_parquet(tmp_path / "left.parquet", {"a": [1]})
    right = _write_parquet(tmp_path / "right.parquet", {"b": [1]})

    with pytest.raises(SystemExit) as exc:
        main(["compare", str(left), str(right)])
    captured = capsys.readouterr()

    assert exc.value.code == 2
    assert "No comparable columns" in captured.err


def test_compare_rtol_suppresses_float_diff(tmp_path: Path) -> None:
    left = _write_parquet(tmp_path / "left.parquet", {"a": [1.0], "b": ["x"]})
    right = _write_parquet(tmp_path / "right.parquet", {"a": [1.001], "b": ["x"]})
    out_path = tmp_path / "diff.parquet"

    exit_code = main(
        [
            "compare",
            str(left),
            str(right),
            "--rtol",
            "0.01",
            "--out",
            str(out_path),
        ]
    )

    assert exit_code == 0
    diff_df = pl.read_parquet(out_path)
    assert diff_df.height == 1
    assert diff_df["global__kind"].to_list() == ["unchanged"]


def test_compare_atol_suppresses_near_zero_float_diff(tmp_path: Path) -> None:
    left = _write_parquet(tmp_path / "left.parquet", {"a": [0.0], "b": ["x"]})
    right = _write_parquet(tmp_path / "right.parquet", {"a": [0.0005], "b": ["x"]})
    out_path = tmp_path / "diff.parquet"

    exit_code = main(
        [
            "compare",
            str(left),
            str(right),
            "--atol",
            "0.001",
            "--out",
            str(out_path),
        ]
    )

    assert exit_code == 0
    diff_df = pl.read_parquet(out_path)
    assert diff_df.height == 1
    assert diff_df["global__kind"].to_list() == ["unchanged"]


def test_compare_warns_on_null_keys(tmp_path: Path, capsys) -> None:
    left = _write_parquet(tmp_path / "left.parquet", {"id": [None, 1], "val": [1, 2]})
    right = _write_parquet(tmp_path / "right.parquet", {"id": [None, 1], "val": [1, 2]})
    out_path = tmp_path / "diff.parquet"

    exit_code = main(["compare", str(left), str(right), "--keys", "id", "--out", str(out_path)])
    captured = capsys.readouterr()

    assert exit_code == 1
    assert "warning:" in captured.err.lower()
    assert "null" in captured.err.lower()


def test_compare_diff_output_sorted_by_keys(tmp_path: Path) -> None:
    left = _write_parquet(tmp_path / "left.parquet", {"id": [3, 1], "val": [30, 10]})
    right = _write_parquet(tmp_path / "right.parquet", {"id": [1, 2], "val": [11, 20]})
    out_path = tmp_path / "diff.parquet"

    exit_code = main(["compare", str(left), str(right), "--keys", "id", "--out", str(out_path)])

    assert exit_code == 1
    diff_df = pl.read_parquet(out_path)
    assert diff_df["id"].to_list() == sorted(diff_df["id"].to_list())
