from __future__ import annotations

import sys
import threading
import time
from pathlib import Path
from unittest import mock

import pytest
from pytest_mock import MockerFixture

from print_dog import cli


class TestBuildParser:
    def test_defaults(self) -> None:
        parser = cli.build_parser()
        ns = parser.parse_args([])
        assert Path(ns.folder) == Path.home() / "Downloads"
        assert ns.log_level == "INFO"
        assert ns.printer is None
        assert ns.print_command is None
        assert ns.dry_run is False
        assert ns.allow_prefixes is None
        assert ns.use_sumatra is False
        assert ns.sumatra_path is None
        assert ns.sumatra_args is None
        assert ns.download_timeout == cli.DEFAULT_DOWNLOAD_TIMEOUT
        assert ns.stable_checks == cli.DEFAULT_STABLE_CHECKS
        assert ns.poll_interval == cli.DEFAULT_POLL_INTERVAL

    def test_multiple_prefixes(self) -> None:
        parser = cli.build_parser()
        ns = parser.parse_args(["--allow-prefix", "Invoice", "--allow-prefix", "Receipt"])
        assert ns.allow_prefixes == ["Invoice", "Receipt"]


class TestSumatraHelpers:
    def test_find_sumatra_prefers_env(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        fake = tmp_path / "SumatraPDF.exe"
        fake.write_text("")
        monkeypatch.setenv("SUMATRAPDF_PATH", str(fake))
        assert cli.find_sumatra_executable(None) == fake

    def test_build_sumatra_command_adds_placeholder(self) -> None:
        exe = Path("/opt/SumatraPDF.exe")
        cmd = cli.build_sumatra_command(exe, None)
        assert "{file}" in cmd
        assert str(exe) in cmd


class TestWaitForDownloadCompletion:
    def test_success_after_stable_size(self, tmp_path: Path) -> None:
        pdf = tmp_path / "sample.pdf"
        pdf.write_bytes(b"data")
        assert cli.wait_for_download_completion(
            pdf,
            timeout=1,
            stable_checks_required=2,
            polling_interval=0.01,
        )

    def test_waits_for_temp_suffix(self, tmp_path: Path) -> None:
        pdf = tmp_path / "slow.pdf"
        temp = tmp_path / "slow.pdf.crdownload"
        pdf.write_bytes(b"payload")
        temp.write_text("tmp")

        def remover() -> None:
            time.sleep(0.05)
            temp.unlink(missing_ok=True)

        threading.Thread(target=remover, daemon=True).start()
        assert cli.wait_for_download_completion(
            pdf,
            timeout=1,
            stable_checks_required=1,
            polling_interval=0.01,
        )

    def test_times_out_if_file_never_appears(self, tmp_path: Path) -> None:
        pdf = tmp_path / "missing.pdf"
        assert not cli.wait_for_download_completion(
            pdf,
            timeout=0.05,
            stable_checks_required=1,
            polling_interval=0.01,
        )


class TestPrintPdf:
    def test_windows_print(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setattr(sys, "platform", "win32")
        called = {}

        def fake_startfile(path: Path, operation: str) -> None:
            called["path"] = Path(path)
            called["operation"] = operation

        monkeypatch.setattr(cli.os, "startfile", fake_startfile, raising=False)
        cli.print_pdf(Path("doc.pdf"), printer=None, print_command=None)
        assert called == {"path": Path("doc.pdf"), "operation": "print"}

    def test_posix_print(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setattr(sys, "platform", "linux")
        monkeypatch.setattr(cli.shutil, "which", lambda name: "lp" if name == "lp" else None)
        result = {}

        def fake_run(cmd: list[str], check: bool) -> None:
            result["cmd"] = cmd
            result["check"] = check

        monkeypatch.setattr(cli.subprocess, "run", fake_run)
        cli.print_pdf(Path("doc.pdf"), printer="HP", print_command=None)
        assert result["cmd"] == ["lp", "-d", "HP", "doc.pdf"]
        assert result["check"] is True

    def test_custom_print_command(self, monkeypatch: pytest.MonkeyPatch) -> None:
        captured = {}

        def fake_run(cmd: list[str], check: bool) -> None:
            captured["cmd"] = cmd

        monkeypatch.setattr(cli.subprocess, "run", fake_run)
        cli.print_pdf(Path("doc.pdf"), printer=None, print_command="echo {file}")
        assert captured["cmd"] == ["echo", "doc.pdf"]


class TestPdfPrintHandler:
    def make_handler(self, **overrides) -> cli.PdfPrintHandler:
        defaults = dict(
            printer=None,
            print_command="echo {file}",
            dry_run=False,
            allowed_prefixes=None,
            download_timeout=0.2,
            stable_checks_required=1,
            polling_interval=0.01,
        )
        defaults.update(overrides)
        return cli.PdfPrintHandler(**defaults)

    def test_skips_non_pdf(self, tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
        caplog.set_level("INFO")
        handler = self.make_handler()
        handler._handle_path(tmp_path / "note.txt")
        assert "Detected new PDF" not in caplog.text

    def test_prefix_filter(self, tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
        caplog.set_level("INFO")
        pdf = tmp_path / "Report-001.pdf"
        handler = self.make_handler(allowed_prefixes=("Invoice",))
        handler._handle_path(pdf)
        assert "Detected new PDF" not in caplog.text

    def test_dry_run_records_printed(self, tmp_path: Path, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch) -> None:
        caplog.set_level("INFO")
        pdf = tmp_path / "Invoice-001.pdf"
        pdf.write_text("content")
        monkeypatch.setattr(cli, "wait_for_download_completion", lambda *args, **kwargs: True)
        handler = self.make_handler(dry_run=True)
        handler._handle_path(pdf)
        assert "Dry run enabled" in caplog.text
        assert pdf.resolve() in handler._printed

    def test_print_failure_logged(self, tmp_path: Path, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch) -> None:
        caplog.set_level("INFO")
        pdf = tmp_path / "Invoice.pdf"
        pdf.write_text("content")
        monkeypatch.setattr(cli, "wait_for_download_completion", lambda *args, **kwargs: True)
        monkeypatch.setattr(cli, "print_pdf", mock.Mock(side_effect=RuntimeError("boom")))
        handler = self.make_handler()
        handler._handle_path(pdf)
        assert "Failed to print" in caplog.text
        assert pdf.resolve() not in handler._printed

    def test_successful_print_marks_printed(self, tmp_path: Path, mocker: MockerFixture) -> None:
        pdf = tmp_path / "Invoice.pdf"
        pdf.write_text("done")
        mocker.patch.object(cli, "wait_for_download_completion", return_value=True)
        mocker.patch.object(cli, "print_pdf", return_value=None)
        handler = self.make_handler()
        handler._handle_path(pdf)
        assert pdf.resolve() in handler._printed


def test_custom_command_windows_path(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(cli.os, "name", "nt")
    captured = {}

    def fake_run(cmd: list[str], check: bool) -> None:
        captured["cmd"] = cmd

    monkeypatch.setattr(cli.subprocess, "run", fake_run)
    cli._print_with_custom_command(
        Path("foo.pdf"),
        '"C://Users//HSC London//SumatraPDF.exe" -print-to-default -silent {file}',
    )
    assert captured["cmd"][0] == "C://Users//HSC London//SumatraPDF.exe"


def test_main_invalid_directory(tmp_path: Path) -> None:
    missing = tmp_path / "missing"
    with pytest.raises(SystemExit):
        cli.main(argv=[str(missing)])
