from __future__ import annotations

import sys
from textwrap import dedent
from typing import TYPE_CHECKING

import pytest

from pipdeptree._cli import RenderContext
from pipdeptree._models import PackageDAG
from pipdeptree._models.package import Package
from pipdeptree._render.graphviz import dump_graphviz, print_graphviz, render_graphviz

if TYPE_CHECKING:
    from collections.abc import Callable, Iterator
    from pathlib import Path
    from unittest.mock import Mock

    from pytest_mock import MockerFixture

    from tests.our_types import MockGraph


def test_render_dot(
    capsys: pytest.CaptureFixture[str],
    example_dag: PackageDAG,
    randomized_example_dag: PackageDAG,
) -> None:
    # Check both the sorted and randomized package tree produces the same sorted graphviz output.
    for package_tree in (example_dag, randomized_example_dag):
        output = dump_graphviz(package_tree, output_format="dot")
        print_graphviz(output)
        out, _ = capsys.readouterr()
        assert out == dedent(
            """\
            digraph {
            \ta -> b [label=">=2.0.0"]
            \ta -> c [label=">=5.7.1"]
            \ta [label="a\\n3.4.0"]
            \tb -> d [label=">=2.30,<2.42"]
            \tb [label="b\\n2.3.1"]
            \tc -> d [label=">=2.30"]
            \tc -> e [label=">=0.12.1"]
            \tc [label="c\\n5.10.0"]
            \td -> e [label=">=0.9.0"]
            \td [label="d\\n2.35"]
            \te [label="e\\n0.12.1"]
            \tf -> b [label=">=2.1.0"]
            \tf [label="f\\n3.1"]
            \tg -> e [label=">=0.9.0"]
            \tg -> f [label=">=3.0.0"]
            \tg [label="g\\n6.8.3rc1"]
            }

            """,
        )


def test_render_pdf(tmp_path: Path, mocker: MockerFixture, example_dag: PackageDAG) -> None:
    output = dump_graphviz(example_dag, output_format="pdf")
    res = tmp_path / "file"
    with pytest.raises(OSError, match="Bad file"):  # noqa: PT012, SIM117 # because we reopen the file
        with res.open("wb") as buf:
            mocker.patch.object(sys, "stdout", buf)
            print_graphviz(output)
    assert res.read_bytes()[:4] == b"%PDF"


def test_render_svg(capsys: pytest.CaptureFixture[str], example_dag: PackageDAG) -> None:
    output = dump_graphviz(example_dag, output_format="svg")
    print_graphviz(output)
    out, _ = capsys.readouterr()
    assert out.startswith("<?xml")
    assert "<svg" in out
    assert out.strip().endswith("</svg>")


def test_render_dot_with_depth(example_dag: PackageDAG) -> None:
    output = dump_graphviz(example_dag, output_format="dot", max_depth=1)
    assert isinstance(output, str)
    # Roots are a and g (depth 0), their direct deps are at depth 1
    # Deeper deps (d) should not appear
    assert "a ->" in output
    assert "g ->" in output
    assert 'd [label="d' not in output  # d is only reachable at depth 2+
    # Direct deps of roots should appear
    assert 'b [label="b' in output
    assert 'c [label="c' in output
    assert 'e [label="e' in output
    assert 'f [label="f' in output


def test_render_dot_reverse_with_depth(example_dag: PackageDAG) -> None:
    reversed_dag = example_dag.reverse()
    output = dump_graphviz(reversed_dag, output_format="dot", is_reverse=True, max_depth=1)
    assert isinstance(output, str)
    # In reverse, root is e (only leaf in forward tree that's not a parent in reverse)
    # At depth 0: e, at depth 1: c, d, g (e's parents)
    assert 'e [label="e' in output
    assert 'c [label="c' in output
    assert 'd [label="d' in output
    assert 'g [label="g' in output
    # Deeper nodes should not appear
    assert 'a [label="a' not in output
    assert 'b [label="b' not in output
    assert 'f [label="f' not in output
    # Edges from e to its parents should exist
    assert "e -> c" in output
    assert "e -> d" in output
    assert "e -> g" in output


def test_render_dot_reverse_infinite_depth(example_dag: PackageDAG) -> None:
    reversed_dag = example_dag.reverse()
    output = dump_graphviz(reversed_dag, output_format="dot", is_reverse=True)
    assert isinstance(output, str)
    # All nodes should appear with no depth limit
    for node in ("a", "b", "c", "d", "e", "f", "g"):
        assert f'{node} [label="' in output
    # Edges should include reverse relationships
    assert "e -> c" in output
    assert "e -> d" in output
    assert "b -> a" in output
    assert "f -> g" in output


def test_render_dot_with_depth_zero(example_dag: PackageDAG) -> None:
    output = dump_graphviz(example_dag, output_format="dot", max_depth=0)
    assert isinstance(output, str)
    # Depth 0 means only root nodes, no edges
    assert "a [label=" in output
    assert "g [label=" in output
    assert "->" not in output


def test_print_graphviz_binary_tty_handling(mocker: MockerFixture, example_dag: PackageDAG) -> None:
    """Test that binary output is written to a temp file when stdout is a tty."""
    output = dump_graphviz(example_dag, output_format="pdf")
    assert isinstance(output, bytes)

    # Mock stdout.isatty() to return True
    mock_stdout = mocker.patch.object(sys, "stdout")
    mock_stdout.isatty.return_value = True
    mock_stdout.fileno.return_value = 1

    # Mock webbrowser.open to avoid actually opening a browser
    mock_open = mocker.patch("webbrowser.open")

    # Capture temp file creation
    class MockTempFile:
        def __init__(self, *_args: object, **_kwargs: object) -> None:
            self._content = b""
            self._name = "/tmp/pipdeptree_test_output.pdf"  # noqa: S108  # Mock path for testing
            self._closed = False

        def write(self, data: bytes) -> None:
            self._content += data

        @property
        def name(self) -> str:
            return self._name

        def __enter__(self) -> "MockTempFile":  # noqa: PYI034, UP037
            return self

        def __exit__(self, *_args: object) -> None:
            self._closed = True

    mocker.patch("tempfile.NamedTemporaryFile", return_value=MockTempFile())

    print_graphviz(output, output_format="pdf")

    # Verify that webbrowser.open was called with the temp file path
    mock_open.assert_called_once_with("/tmp/pipdeptree_test_output.pdf")  # noqa: S108  # Mock path for testing


def test_print_graphviz_binary_non_tty_handling(mocker: MockerFixture, example_dag: PackageDAG) -> None:
    """Test that binary output is written directly to stdout when stdout is not a tty."""
    output = dump_graphviz(example_dag, output_format="pdf")
    assert isinstance(output, bytes)

    # Mock stdout.isatty() to return False (non-tty, e.g., piped output)
    mock_stdout = mocker.patch.object(sys, "stdout")
    mock_stdout.isatty.return_value = False
    mock_stdout.fileno.return_value = 1

    # Mock fdopen to capture binary write
    mock_fdopen = mocker.patch("os.fdopen")
    mock_bytestream = mocker.MagicMock()
    mock_fdopen.return_value.__enter__.return_value = mock_bytestream

    print_graphviz(output, output_format="pdf")

    # Verify that binary data was written to stdout
    mock_fdopen.assert_called_once_with(1, "wb")
    mock_bytestream.write.assert_called_once_with(output)


def test_render_graphviz_with_metadata(
    mock_pkgs: Callable[[MockGraph], Iterator[Mock]],
    capsys: pytest.CaptureFixture[str],
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    graph: MockGraph = {("a", "1.0"): [("b", [(">=", "1.0")])], ("b", "1.0"): []}
    dag = PackageDAG.from_pkgs(list(mock_pkgs(graph)))
    monkeypatch.setattr(Package, "licenses", lambda _: "(MIT)")
    ctx = RenderContext(metadata=["license"])
    render_graphviz(dag, output_format="dot", reverse=False, context=ctx)
    output = capsys.readouterr().out
    assert "MIT License" in output


def test_render_graphviz_reversed_with_metadata(
    mock_pkgs: Callable[[MockGraph], Iterator[Mock]],
    capsys: pytest.CaptureFixture[str],
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    graph: MockGraph = {("a", "1.0"): [("b", [(">=", "1.0")])], ("b", "1.0"): []}
    dag = PackageDAG.from_pkgs(list(mock_pkgs(graph)))
    monkeypatch.setattr(Package, "licenses", lambda _: "(MIT)")
    ctx = RenderContext(metadata=["license"])
    render_graphviz(dag.reverse(), output_format="dot", reverse=True, context=ctx)
    output = capsys.readouterr().out
    assert "MIT License" in output
