#!/usr/bin/python3
"""Tests for slpkg.history.History and slpkg.history_logger.HistoryLogger."""

from __future__ import annotations

import json
import re
from pathlib import Path
from unittest.mock import patch

import pytest
from slpkg.history import History
from slpkg.history_logger import HistoryLogger

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def make_history(log_file: Path, limit: int = 0, package: str = "", option_json: bool = False) -> History:
    """Construct History bypassing __init__ (avoids config_load)."""
    h: History = object.__new__(History)
    h.history_log_file = log_file
    h.limit = limit
    h.package = package.lower()
    h.option_json = option_json
    h.green = ""
    h.yellow = ""
    h.red = ""
    h.cyan = ""
    h.grey = ""
    h.bold = ""
    h.endc = ""
    return h


def make_history_logger(log_file: Path) -> HistoryLogger:
    """Construct HistoryLogger bypassing __init__ (avoids config_load)."""
    hl: HistoryLogger = object.__new__(HistoryLogger)
    hl.history_log_file = log_file
    return hl


def _entry(timestamp: str, action: str, name: str, version: str, repo: str) -> str:
    """Build a log line matching the format written by HistoryLogger.write()."""
    return f"{timestamp} | {action:<9} | {name:<30} | {version:<25} | {repo}\n"


# ---------------------------------------------------------------------------
# History.clear()
# ---------------------------------------------------------------------------


def test_clear_deletes_existing_file(tmp_path: Path) -> None:
    f = tmp_path / "history.log"
    f.write_text("something", encoding="utf-8")
    make_history(f).clear()
    assert not f.exists()


def test_clear_no_error_if_file_missing(tmp_path: Path) -> None:
    f = tmp_path / "history.log"
    make_history(f).clear()  # must not raise
    assert not f.exists()


# ---------------------------------------------------------------------------
# History.view() — empty / missing file
# ---------------------------------------------------------------------------


def test_view_missing_file_prints_no_history(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    make_history(tmp_path / "history.log").view()
    assert "No package operation history found." in capsys.readouterr().out


def test_view_empty_file_prints_no_history(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    f = tmp_path / "history.log"
    f.write_text("", encoding="utf-8")
    make_history(f).view()
    assert "No package operation history found." in capsys.readouterr().out


def test_view_unparseable_lines_prints_no_history(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    f = tmp_path / "history.log"
    f.write_text("no pipes here\nnor here\n", encoding="utf-8")
    make_history(f).view()
    assert "No package operation history found." in capsys.readouterr().out


# ---------------------------------------------------------------------------
# History.view() — ordering (oldest first)
# ---------------------------------------------------------------------------


def test_view_oldest_entry_appears_first(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    f = tmp_path / "history.log"
    f.write_text(
        _entry("2024-01-01 10:00", "install", "zzz", "1.0", "sbo")
        + _entry("2024-01-02 11:00", "remove", "aaa", "2.0", "sbo"),
        encoding="utf-8",
    )
    make_history(f).view()
    out = capsys.readouterr().out
    assert out.index("zzz") < out.index("aaa")


def test_view_single_entry_displayed(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    f = tmp_path / "history.log"
    f.write_text(_entry("2024-06-15 08:30", "install", "vim", "9.0", "alien"), encoding="utf-8")
    make_history(f).view()
    out = capsys.readouterr().out
    assert "vim" in out
    assert "install" in out


# ---------------------------------------------------------------------------
# History.view() — limit
# ---------------------------------------------------------------------------


def test_view_limit_takes_last_n_entries(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    """With limit=2 only the 2 most recent (bottom of file) entries are shown."""
    f = tmp_path / "history.log"
    f.write_text(
        _entry("2024-01-01 08:00", "install", "pkg1", "1.0", "sbo")
        + _entry("2024-01-02 08:00", "upgrade", "pkg2", "2.0", "sbo")
        + _entry("2024-01-03 08:00", "reinstall", "pkg3", "3.0", "sbo")
        + _entry("2024-01-04 08:00", "remove", "pkg4", "4.0", "sbo")
        + _entry("2024-01-05 08:00", "build", "pkg5", "5.0", "sbo"),
        encoding="utf-8",
    )
    make_history(f, limit=2).view()
    out = capsys.readouterr().out
    assert "pkg4" in out
    assert "pkg5" in out
    assert "pkg1" not in out
    assert "pkg2" not in out
    assert "pkg3" not in out


def test_view_limit_zero_shows_all(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    f = tmp_path / "history.log"
    f.write_text(
        "".join(_entry(f"2024-01-0{i} 08:00", "install", f"pkg{i}", f"{i}.0", "sbo") for i in range(1, 5)),
        encoding="utf-8",
    )
    make_history(f, limit=0).view()
    out = capsys.readouterr().out
    for i in range(1, 5):
        assert f"pkg{i}" in out


def test_view_limit_larger_than_total_shows_all(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    f = tmp_path / "history.log"
    f.write_text(_entry("2024-01-01 08:00", "install", "only_pkg", "1.0", "sbo"), encoding="utf-8")
    make_history(f, limit=100).view()
    assert "only_pkg" in capsys.readouterr().out


# ---------------------------------------------------------------------------
# History.view() — date grouping
# ---------------------------------------------------------------------------


def test_view_same_date_grouped_under_one_header(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    f = tmp_path / "history.log"
    f.write_text(
        _entry("2024-03-10 09:00", "install", "pkgA", "1.0", "sbo")
        + _entry("2024-03-10 10:00", "upgrade", "pkgB", "2.0", "sbo"),
        encoding="utf-8",
    )
    make_history(f).view()
    assert capsys.readouterr().out.count("2024-03-10") == 1


def test_view_different_dates_each_gets_header(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    f = tmp_path / "history.log"
    f.write_text(
        _entry("2024-03-10 09:00", "install", "pkgA", "1.0", "sbo")
        + _entry("2024-03-11 10:00", "upgrade", "pkgB", "2.0", "sbo"),
        encoding="utf-8",
    )
    make_history(f).view()
    out = capsys.readouterr().out
    assert "2024-03-10" in out
    assert "2024-03-11" in out


# ---------------------------------------------------------------------------
# History.view() — truncation
# ---------------------------------------------------------------------------


def test_view_long_name_truncated(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    long_name = "a" * 30  # _NW is 24
    f = tmp_path / "history.log"
    f.write_text(_entry("2024-01-01 09:00", "install", long_name, "1.0", "sbo"), encoding="utf-8")
    make_history(f).view()
    out = capsys.readouterr().out
    assert "..." in out
    assert long_name not in out


def test_view_long_version_truncated(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    long_ver = "9" * 25  # _VW is 18
    f = tmp_path / "history.log"
    f.write_text(_entry("2024-01-01 09:00", "install", "pkg", long_ver, "sbo"), encoding="utf-8")
    make_history(f).view()
    out = capsys.readouterr().out
    assert "..." in out
    assert long_ver not in out


# ---------------------------------------------------------------------------
# History.view() — footer
# ---------------------------------------------------------------------------


def test_view_footer_shows_total_count(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    f = tmp_path / "history.log"
    f.write_text(
        _entry("2024-01-01 08:00", "install", "p1", "1.0", "sbo")
        + _entry("2024-01-02 08:00", "install", "p2", "2.0", "sbo")
        + _entry("2024-01-03 08:00", "install", "p3", "3.0", "sbo"),
        encoding="utf-8",
    )
    make_history(f).view()
    assert "3 entries" in capsys.readouterr().out


def test_view_footer_with_limit_shows_shown_of_total(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    f = tmp_path / "history.log"
    f.write_text(
        "".join(_entry(f"2024-01-0{i} 08:00", "install", f"p{i}", f"{i}.0", "sbo") for i in range(1, 6)),
        encoding="utf-8",
    )
    make_history(f, limit=3).view()
    assert "3 of 5" in capsys.readouterr().out


def test_view_single_entry_footer_uses_singular(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    f = tmp_path / "history.log"
    f.write_text(_entry("2024-01-01 08:00", "install", "p1", "1.0", "sbo"), encoding="utf-8")
    make_history(f).view()
    out = capsys.readouterr().out
    assert "1 entry" in out
    assert "entries" not in out


# ---------------------------------------------------------------------------
# HistoryLogger.write()
# ---------------------------------------------------------------------------


def test_write_creates_file_with_entry(tmp_path: Path) -> None:
    f = tmp_path / "history.log"
    make_history_logger(f).write("install", "vim", "vim-9.0-x86_64-1.txz", "alien")
    assert f.exists()
    content = f.read_text(encoding="utf-8")
    assert "install" in content
    assert "vim" in content


def test_write_appends_multiple_entries(tmp_path: Path) -> None:
    f = tmp_path / "history.log"
    hl = make_history_logger(f)
    hl.write("install", "vim", "vim-9.0-x86_64-1.txz", "alien")
    hl.write("remove", "gcc", "gcc-13.0-x86_64-1.txz", "slackware")
    lines = [ln for ln in f.read_text(encoding="utf-8").splitlines() if ln.strip()]
    assert len(lines) == 2


def test_write_version_extracted_from_package(tmp_path: Path) -> None:
    f = tmp_path / "history.log"
    make_history_logger(f).write("install", "gnupg2", "gnupg2-2.5.18-x86_64-1.txz", "alien")
    assert "2.5.18-x86_64-1" in f.read_text(encoding="utf-8")


def test_write_version_fallback_when_no_name_prefix(tmp_path: Path) -> None:
    """If the stem doesn't start with 'name-', the whole stem is used as version."""
    f = tmp_path / "history.log"
    make_history_logger(f).write("install", "mypkg", "unexpected-filename.txz", "sbo")
    assert "unexpected-filename" in f.read_text(encoding="utf-8")


def test_write_timestamp_format(tmp_path: Path) -> None:
    f = tmp_path / "history.log"
    make_history_logger(f).write("install", "vim", "vim-9.0-x86_64-1.txz", "alien")
    timestamp = f.read_text(encoding="utf-8").splitlines()[0].split("|")[0].strip()
    assert re.match(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}", timestamp)


def test_write_produces_five_pipe_separated_fields(tmp_path: Path) -> None:
    f = tmp_path / "history.log"
    make_history_logger(f).write("upgrade", "ffmpeg", "ffmpeg-7.1-x86_64-1.txz", "alien")
    line = f.read_text(encoding="utf-8").splitlines()[0]
    parts = [p.strip() for p in line.split("|")]
    assert len(parts) == 5


def test_write_oserror_does_not_raise(tmp_path: Path) -> None:
    """OSError during write is caught and logged, not propagated."""
    f = tmp_path / "history.log"
    hl = make_history_logger(f)
    with patch("pathlib.Path.open", side_effect=OSError("disk full")):
        hl.write("install", "vim", "vim-9.0-x86_64-1.txz", "alien")  # must not raise


# ---------------------------------------------------------------------------
# History.view() — package filter
# ---------------------------------------------------------------------------


def test_view_package_filter_shows_only_matching(tmp_path: Path, capsys) -> None:
    f = tmp_path / "history.log"
    f.write_text(
        _entry("2026-03-08 10:00:00", "install", "ffmpeg", "6.1", "sbo") +
        _entry("2026-03-08 10:01:00", "install", "vlc", "3.0", "sbo") +
        _entry("2026-03-08 10:02:00", "upgrade", "ffmpeg", "7.0", "sbo"),
        encoding="utf-8",
    )
    make_history(f, package="ffmpeg").view()
    out = capsys.readouterr().out
    assert "ffmpeg" in out
    assert "vlc" not in out


def test_view_package_filter_case_insensitive(tmp_path: Path, capsys) -> None:
    f = tmp_path / "history.log"
    f.write_text(
        _entry("2026-03-08 10:00:00", "install", "FFmpeg", "6.1", "sbo"),
        encoding="utf-8",
    )
    make_history(f, package="ffmpeg").view()
    out = capsys.readouterr().out
    assert "FFmpeg" in out


def test_view_package_filter_no_match_shows_empty(tmp_path: Path, capsys) -> None:
    f = tmp_path / "history.log"
    f.write_text(
        _entry("2026-03-08 10:00:00", "install", "vlc", "3.0", "sbo"),
        encoding="utf-8",
    )
    make_history(f, package="ffmpeg").view()
    out = capsys.readouterr().out
    assert "vlc" not in out
    assert "0 entries" in out


def test_view_package_filter_header_shows_package_name(tmp_path: Path, capsys) -> None:
    f = tmp_path / "history.log"
    f.write_text(
        _entry("2026-03-08 10:00:00", "install", "ffmpeg", "6.1", "sbo"),
        encoding="utf-8",
    )
    make_history(f, package="ffmpeg").view()
    out = capsys.readouterr().out
    assert "ffmpeg" in out.splitlines()[0]


# ---------------------------------------------------------------------------
# History.view() — --json output
# ---------------------------------------------------------------------------


def test_view_json_is_valid_json(tmp_path: Path, capsys) -> None:
    f = tmp_path / "history.log"
    f.write_text(
        _entry("2026-03-08 10:00", "install", "ffmpeg", "7.1", "sbo"),
        encoding="utf-8",
    )
    make_history(f, option_json=True).view()
    data = json.loads(capsys.readouterr().out)
    assert isinstance(data, list)


def test_view_json_contains_expected_keys(tmp_path: Path, capsys) -> None:
    f = tmp_path / "history.log"
    f.write_text(
        _entry("2026-03-08 10:00", "install", "ffmpeg", "7.1", "sbo"),
        encoding="utf-8",
    )
    make_history(f, option_json=True).view()
    data = json.loads(capsys.readouterr().out)
    for key in ("timestamp", "date", "time", "action", "name", "version", "repository"):
        assert key in data[0], f"Missing key: {key}"


def test_view_json_entry_values(tmp_path: Path, capsys) -> None:
    f = tmp_path / "history.log"
    f.write_text(
        _entry("2026-03-08 10:00", "install", "ffmpeg", "7.1", "sbo"),
        encoding="utf-8",
    )
    make_history(f, option_json=True).view()
    data = json.loads(capsys.readouterr().out)
    assert data[0]["action"] == "install"
    assert data[0]["name"] == "ffmpeg"
    assert data[0]["version"] == "7.1"
    assert data[0]["repository"] == "sbo"
    assert data[0]["date"] == "2026-03-08"


def test_view_json_multiple_entries(tmp_path: Path, capsys) -> None:
    f = tmp_path / "history.log"
    f.write_text(
        _entry("2026-03-08 10:00", "install", "ffmpeg", "7.1", "sbo") +
        _entry("2026-03-08 10:01", "upgrade", "vlc", "3.0", "alien"),
        encoding="utf-8",
    )
    make_history(f, option_json=True).view()
    data = json.loads(capsys.readouterr().out)
    assert len(data) == 2
    assert data[0]["name"] == "ffmpeg"
    assert data[1]["name"] == "vlc"


def test_view_json_empty_file_outputs_empty_array(tmp_path: Path, capsys) -> None:
    f = tmp_path / "history.log"
    f.write_text("", encoding="utf-8")
    make_history(f, option_json=True).view()
    out = capsys.readouterr().out
    assert "No package operation history found." in out


def test_view_json_limit_applied(tmp_path: Path, capsys) -> None:
    f = tmp_path / "history.log"
    f.write_text(
        _entry("2026-03-08 08:00", "install", "pkg1", "1.0", "sbo") +
        _entry("2026-03-08 09:00", "install", "pkg2", "2.0", "sbo") +
        _entry("2026-03-08 10:00", "install", "pkg3", "3.0", "sbo"),
        encoding="utf-8",
    )
    make_history(f, limit=2, option_json=True).view()
    data = json.loads(capsys.readouterr().out)
    assert len(data) == 2
    assert data[0]["name"] == "pkg2"
    assert data[1]["name"] == "pkg3"


def test_view_json_package_filter(tmp_path: Path, capsys) -> None:
    f = tmp_path / "history.log"
    f.write_text(
        _entry("2026-03-08 10:00", "install", "ffmpeg", "7.1", "sbo") +
        _entry("2026-03-08 10:01", "install", "vlc", "3.0", "alien"),
        encoding="utf-8",
    )
    make_history(f, package="ffmpeg", option_json=True).view()
    data = json.loads(capsys.readouterr().out)
    assert len(data) == 1
    assert data[0]["name"] == "ffmpeg"
