# Copyright (C) 2026 NPO NT
# SPDX-License-Identifier: AGPL-3.0-only

from __future__ import annotations

from pathlib import Path
from types import SimpleNamespace

import pytest

from mika_deid.gui import MAX_ZOOM_FACTOR, MIN_ZOOM_FACTOR, ReviewApp, _short_display_path
from mika_deid.models import Action, Box, Finding, Severity


class StubVar:
    def __init__(self) -> None:
        self.value = ""

    def set(self, value: str) -> None:
        self.value = value


def _review_app() -> ReviewApp:
    app = ReviewApp.__new__(ReviewApp)
    app.analysis = SimpleNamespace(findings=[])
    app.page_index = 0
    app.busy = False
    app.drag_start = None
    app.drag_item = None
    app.manual_history = []
    app.manual_sequence = 0
    app.selected_finding_id = None
    app.confirmed_pages = {0}
    app.last_output_path = None
    app.status_var = StubVar()
    app.output_path_var = StubVar()
    app.draw_page = lambda **_kwargs: None
    app._update_controls = lambda: None
    return app


def _manual_finding(finding_id: str = "manual-test", page_index: int = 0) -> Finding:
    return Finding(
        finding_id=finding_id,
        page_index=page_index,
        category="manual",
        box=Box(10, 20, 80, 90),
        action=Action.REDACT,
        severity=Severity.CRITICAL,
        confidence=100.0,
        source="manual",
        reason="synthetic test",
        manually_added=True,
    )


def test_accidental_manual_region_can_be_undone() -> None:
    app = _review_app()
    app.drag_start = (10.0, 20.0)
    app._canvas_to_page = lambda _x, _y: (80.0, 90.0)

    app.on_canvas_release(SimpleNamespace(x=0, y=0))

    assert len(app.analysis.findings) == 1
    finding = app.analysis.findings[0]
    assert finding.manually_added
    assert finding.selected
    assert app.selected_finding_id == finding.finding_id
    assert app.confirmed_pages == set()

    app.undo_last_manual()

    assert app.analysis.findings == []
    assert app.selected_finding_id is None
    assert app.manual_history == []
    assert "отменено" in app.status_var.value


def test_repeated_manual_boxes_receive_distinct_ids() -> None:
    app = _review_app()
    app._canvas_to_page = lambda _x, _y: (80.0, 90.0)

    for _index in range(2):
        app.drag_start = (10.0, 20.0)
        app.on_canvas_release(SimpleNamespace(x=0, y=0))

    assert len(app.analysis.findings) == 2
    assert len({finding.finding_id for finding in app.analysis.findings}) == 2


def test_delete_button_removes_manual_region_but_not_automatic_finding() -> None:
    app = _review_app()
    automatic = Finding(
        finding_id="automatic-test",
        page_index=0,
        category="person_name",
        box=Box(1, 2, 30, 40),
        action=Action.REDACT,
        severity=Severity.CRITICAL,
        confidence=95.0,
        source="native",
        reason="synthetic test",
    )
    manual = _manual_finding()
    app.analysis.findings.extend([automatic, manual])

    app.selected_finding_id = automatic.finding_id
    app.delete_selected_manual()
    assert app.analysis.findings == [automatic, manual]

    app.selected_finding_id = manual.finding_id
    app.delete_selected_manual()
    assert app.analysis.findings == [automatic]
    assert app.selected_finding_id is None


def test_choose_output_path_retains_explicit_pdf_destination(monkeypatch: pytest.MonkeyPatch) -> None:
    app = _review_app()
    app.pending_output_path = None
    app.output_path_var = StubVar()
    destination = "/synthetic/folder/clean-copy.pdf"
    monkeypatch.setattr("mika_deid.gui.filedialog.asksaveasfilename", lambda **_kwargs: destination)

    assert app.choose_output_path() == destination
    assert app.pending_output_path == destination
    assert _short_display_path(destination) in app.output_path_var.value
    assert "не будет перезаписан" in app.output_path_var.value


def test_tree_refresh_event_does_not_start_a_redraw_loop() -> None:
    app = _review_app()
    app.refreshing_tree = False
    app.selected_finding_id = "manual-test"
    app.finding_tree = SimpleNamespace(selection=lambda: ("manual-test",))
    calls = {"draw": 0, "controls": 0}
    app.draw_page = lambda **_kwargs: calls.__setitem__("draw", calls["draw"] + 1)
    app._update_selection_controls = lambda: calls.__setitem__("controls", calls["controls"] + 1)

    app.select_tree_finding(SimpleNamespace())

    assert calls == {"draw": 0, "controls": 1}


def test_cleaned_copy_can_be_opened_or_revealed_on_macos(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    app = _review_app()
    output = tmp_path / "synthetic-clean.pdf"
    output.write_bytes(b"synthetic")
    commands: list[list[str]] = []
    monkeypatch.setattr("mika_deid.gui.sys.platform", "darwin")

    def fake_run(command, **_kwargs):
        commands.append(command)
        return SimpleNamespace(returncode=0, stderr="")

    monkeypatch.setattr(
        "mika_deid.gui.subprocess.run",
        fake_run,
    )

    app._open_local_path(output)
    app._open_local_path(output, reveal=True)

    assert commands == [
        ["/usr/bin/open", str(output)],
        ["/usr/bin/open", "-R", str(output)],
    ]


def test_open_action_prompts_for_an_existing_pdf_after_restart(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    app = _review_app()
    output = tmp_path / "previously-created-clean-copy.pdf"
    output.write_bytes(b"synthetic")
    opened: list[tuple[Path, bool]] = []
    monkeypatch.setattr(
        "mika_deid.gui.filedialog.askopenfilename",
        lambda **_kwargs: str(output),
    )
    app._open_local_path = lambda path, reveal=False: opened.append((path, reveal))

    app.open_last_output()

    assert app.last_output_path == output
    assert opened == [(output, False)]
    assert _short_display_path(output) in app.output_path_var.value


def test_reveal_action_uses_current_copy_without_another_dialog(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    app = _review_app()
    output = tmp_path / "current-clean-copy.pdf"
    output.write_bytes(b"synthetic")
    app.last_output_path = output
    opened: list[tuple[Path, bool]] = []
    monkeypatch.setattr(
        "mika_deid.gui.filedialog.askopenfilename",
        lambda **_kwargs: pytest.fail("unexpected file dialog"),
    )
    app._open_local_path = lambda path, reveal=False: opened.append((path, reveal))

    app.reveal_last_output()

    assert opened == [(output, True)]


def test_zoom_controls_are_bounded_and_keep_the_focus_point() -> None:
    app = _review_app()
    app.zoom_factor = 1.0
    drawn_focuses: list[tuple[float, float] | None] = []
    app._viewport_center_page = lambda: (123.0, 456.0)
    app.draw_page = lambda **kwargs: drawn_focuses.append(kwargs.get("focus_point"))

    for _index in range(20):
        app.zoom_in()
    assert app.zoom_factor == MAX_ZOOM_FACTOR
    assert drawn_focuses[-1] == (123.0, 456.0)

    for _index in range(40):
        app.zoom_out()
    assert app.zoom_factor == MIN_ZOOM_FACTOR

    app.zoom_fit()
    assert app.zoom_factor == 1.0
