"""Tests for src.cli_reconcile — Typer CLI for two-source reconciliation.

The reconciliation engine itself is covered by ``test_reconcile.py``;
this file exercises the CLI surface around it: argument parsing
(comma-separated keys, optional dates), preview vs. apply modes, the
four output files, and error paths for bad inputs.
"""

from __future__ import annotations

import sys
from pathlib import Path

import pandas as pd
import pytest
from typer.testing import CliRunner

from src.cli_reconcile import app


runner = CliRunner()


def _write_bank(path: Path) -> None:
    """Bank-feed-shaped CSV with two transactions."""
    path.write_text(
        "date,amount,desc\n"
        "2026-01-05,100.00,ACME\n"
        "2026-01-06,250.00,WIDGET CO\n"
    )


def _write_ledger(path: Path) -> None:
    """Ledger-shaped CSV with the same two transactions under
    different column names — exercises the rename-on-match path."""
    path.write_text(
        "posted,amt,memo\n"
        "2026-01-05,100.00,Acme Inc\n"
        "2026-01-06,250.00,Widget\n"
    )


class TestPreviewMode:
    """Default mode (no ``--apply``): print stats only, write nothing."""

    def test_basic_preview_succeeds(self, tmp_path):
        bank = tmp_path / "bank.csv"
        ledger = tmp_path / "ledger.csv"
        _write_bank(bank)
        _write_ledger(ledger)
        result = runner.invoke(app, [
            str(bank), str(ledger),
            "--left-amount", "amount", "--right-amount", "amt",
            "--left-date", "date", "--right-date", "posted",
        ])
        assert result.exit_code == 0, result.stdout
        assert "Matched:" in result.stdout
        assert "Unmatched left:" in result.stdout
        # Two-of-two match in the fixture.
        assert "Matched:             2" in result.stdout
        # The reminder banner is part of the preview UX.
        assert "Add --apply" in result.stdout

    def test_preview_does_not_write_files(self, tmp_path):
        bank = tmp_path / "bank.csv"
        ledger = tmp_path / "ledger.csv"
        _write_bank(bank)
        _write_ledger(ledger)
        runner.invoke(app, [
            str(bank), str(ledger),
            "--left-amount", "amount", "--right-amount", "amt",
            "--left-date", "date", "--right-date", "posted",
        ])
        # None of the four output suffixes should land beside the input.
        for suffix in ("matched", "unmatched_left", "unmatched_right", "review"):
            assert not (tmp_path / f"bank_{suffix}.csv").exists()


class TestApplyMode:
    """``--apply``: write the four output files beside the LEFT input."""

    def test_apply_writes_four_files(self, tmp_path):
        bank = tmp_path / "bank.csv"
        ledger = tmp_path / "ledger.csv"
        _write_bank(bank)
        _write_ledger(ledger)
        result = runner.invoke(app, [
            str(bank), str(ledger),
            "--left-amount", "amount", "--right-amount", "amt",
            "--left-date", "date", "--right-date", "posted",
            "--apply",
        ])
        assert result.exit_code == 0, result.stdout
        # All four output files land beside the left input, sharing
        # its stem.
        for suffix in ("matched", "unmatched_left", "unmatched_right", "review"):
            out = tmp_path / f"bank_{suffix}.csv"
            assert out.exists(), f"missing {out.name}"
        # Matched.csv carries the two pairs.
        matched = pd.read_csv(tmp_path / "bank_matched.csv")
        assert len(matched) == 2

    def test_apply_with_unmatched_rows(self, tmp_path):
        bank = tmp_path / "bank.csv"
        ledger = tmp_path / "ledger.csv"
        bank.write_text(
            "date,amount,desc\n"
            "2026-01-05,100.00,ACME\n"
            "2026-01-07,99.99,LEFT-ONLY\n"
        )
        ledger.write_text(
            "posted,amt,memo\n"
            "2026-01-05,100.00,Acme\n"
            "2026-01-08,500.00,RIGHT-ONLY\n"
        )
        result = runner.invoke(app, [
            str(bank), str(ledger),
            "--left-amount", "amount", "--right-amount", "amt",
            "--left-date", "date", "--right-date", "posted",
            "--apply",
        ])
        assert result.exit_code == 0
        unmatched_l = pd.read_csv(tmp_path / "bank_unmatched_left.csv")
        unmatched_r = pd.read_csv(tmp_path / "bank_unmatched_right.csv")
        assert "LEFT-ONLY" in unmatched_l["desc"].tolist()
        assert "RIGHT-ONLY" in unmatched_r["memo"].tolist()


class TestToleranceFlags:
    def test_amount_tolerance_absorbs_rounding(self, tmp_path):
        bank = tmp_path / "bank.csv"
        ledger = tmp_path / "ledger.csv"
        bank.write_text(
            "date,amount,desc\n"
            "2026-01-05,100.00,ACME\n"
        )
        ledger.write_text(
            "posted,amt,memo\n"
            "2026-01-05,100.02,Acme\n"
        )
        # Without tolerance: no match.
        result_no_tol = runner.invoke(app, [
            str(bank), str(ledger),
            "--left-amount", "amount", "--right-amount", "amt",
            "--left-date", "date", "--right-date", "posted",
        ])
        assert "Matched:             0" in result_no_tol.stdout
        # With tolerance: one match.
        result_with_tol = runner.invoke(app, [
            str(bank), str(ledger),
            "--left-amount", "amount", "--right-amount", "amt",
            "--left-date", "date", "--right-date", "posted",
            "--amount-tolerance", "0.05",
        ])
        assert "Matched:             1" in result_with_tol.stdout

    def test_date_tolerance_allows_drift(self, tmp_path):
        bank = tmp_path / "bank.csv"
        ledger = tmp_path / "ledger.csv"
        bank.write_text(
            "date,amount,desc\n"
            "2026-01-05,100.00,ACME\n"
        )
        ledger.write_text(
            "posted,amt,memo\n"
            "2026-01-07,100.00,Acme\n"  # 2-day drift
        )
        result = runner.invoke(app, [
            str(bank), str(ledger),
            "--left-amount", "amount", "--right-amount", "amt",
            "--left-date", "date", "--right-date", "posted",
            "--date-tolerance", "3",
        ])
        assert "Matched:             1" in result.stdout


class TestSignInversion:
    def test_invert_right_sign(self, tmp_path):
        bank = tmp_path / "bank.csv"
        ledger = tmp_path / "ledger.csv"
        bank.write_text(
            "date,amount,desc\n"
            "2026-01-05,100.00,ACME\n"
        )
        ledger.write_text(
            "posted,amt,memo\n"
            "2026-01-05,-100.00,Acme\n"  # sign convention flipped
        )
        result = runner.invoke(app, [
            str(bank), str(ledger),
            "--left-amount", "amount", "--right-amount", "amt",
            "--left-date", "date", "--right-date", "posted",
            "--invert-right-sign",
        ])
        assert "Matched:             1" in result.stdout


class TestKeyFlags:
    def test_comma_separated_keys_pair_off(self, tmp_path):
        # Same check number, mismatched posting dates — the date-only
        # pass would miss but the key match catches.
        bank = tmp_path / "bank.csv"
        ledger = tmp_path / "ledger.csv"
        bank.write_text(
            "date,amount,desc,check_no\n"
            "2026-01-05,100.00,ACME,1042\n"
        )
        ledger.write_text(
            "posted,amt,memo,ref\n"
            "2026-01-12,100.00,Acme,1042\n"  # 7-day drift
        )
        result = runner.invoke(app, [
            str(bank), str(ledger),
            "--left-amount", "amount", "--right-amount", "amt",
            "--left-date", "date", "--right-date", "posted",
            "--left-keys", "check_no",
            "--right-keys", "ref",
        ])
        assert "Matched:             1" in result.stdout


class TestErrorPaths:
    def test_missing_left_file(self, tmp_path):
        ledger = tmp_path / "ledger.csv"
        _write_ledger(ledger)
        result = runner.invoke(app, [
            str(tmp_path / "nope.csv"), str(ledger),
            "--left-amount", "amount", "--right-amount", "amt",
        ])
        assert result.exit_code != 0
        assert "not found" in result.stdout.lower() or "not found" in (result.stderr or "").lower()

    def test_missing_right_file(self, tmp_path):
        bank = tmp_path / "bank.csv"
        _write_bank(bank)
        result = runner.invoke(app, [
            str(bank), str(tmp_path / "nope.csv"),
            "--left-amount", "amount", "--right-amount", "amt",
        ])
        assert result.exit_code != 0

    def test_unknown_amount_column_surfaces_value_error(self, tmp_path):
        # The reconcile engine raises ValueError on unknown column names;
        # the CLI catches it and exits 1 with a readable banner.
        bank = tmp_path / "bank.csv"
        ledger = tmp_path / "ledger.csv"
        _write_bank(bank)
        _write_ledger(ledger)
        result = runner.invoke(app, [
            str(bank), str(ledger),
            "--left-amount", "NOT_A_COLUMN", "--right-amount", "amt",
        ])
        assert result.exit_code == 1
        # Banner format: "Error: <message>"
        assert "Error" in result.stdout or "Error" in (result.stderr or "")

    def test_help_renders(self):
        # ``--help`` must work — examples in docstrings reference it.
        result = runner.invoke(app, ["--help"])
        assert result.exit_code == 0
        assert "reconcile" in result.stdout.lower()


class TestExcelInput:
    """Input may be CSV, TSV, or Excel — read_file dispatches by suffix."""

    def test_excel_left_file_reads(self, tmp_path):
        bank = tmp_path / "bank.xlsx"
        df = pd.DataFrame({
            "date": ["2026-01-05"],
            "amount": [100.00],
            "desc": ["ACME"],
        })
        df.to_excel(bank, index=False)
        ledger = tmp_path / "ledger.csv"
        _write_ledger(ledger)
        result = runner.invoke(app, [
            str(bank), str(ledger),
            "--left-amount", "amount", "--right-amount", "amt",
            "--left-date", "date", "--right-date", "posted",
        ])
        assert result.exit_code == 0, result.stdout
        # 1 of 1 left rows matched against the 2-row right ledger.
        assert "Matched:             1" in result.stdout
