"""Tests for the depwhy CLI (typer app)."""

from __future__ import annotations

import json
from unittest.mock import patch

import pytest
from typer.testing import CliRunner

from depwhy.cli import app
from depwhy.graph.models import Conflict, ConflictReport, Constraint, FixCandidate

runner = CliRunner()

# ── Fixtures ─────────────────────────────────────────────────


@pytest.fixture()
def conflict_report() -> ConflictReport:
    """A ConflictReport with one realistic conflict."""
    return ConflictReport(
        has_conflicts=True,
        conflicts=[
            Conflict(
                package="urllib3",
                problem=(
                    "requests 2.28.0 needs urllib3>=1.21.1,<1.27 but you pinned urllib3==2.0.0"
                ),
                constraints=[
                    Constraint(
                        package="urllib3",
                        specifier=">=1.21.1,<1.27",
                        required_by="requests==2.28.0",
                        chain=["your-project", "requests==2.28.0", "urllib3>=1.21.1,<1.27"],
                    ),
                    Constraint(
                        package="urllib3",
                        specifier="==2.0.0",
                        required_by="your-project",
                        chain=["your-project", "urllib3==2.0.0"],
                    ),
                ],
                solution=FixCandidate(
                    description="Remove your pin — use urllib3==1.26.18 (latest compatible)",
                    pip_command="pip install urllib3==1.26.18",
                    is_alternative=False,
                ),
                alternative=FixCandidate(
                    description="Upgrade requests to >=2.31.0 (supports urllib3 2.x)",
                    pip_command="pip install requests>=2.31.0",
                    is_alternative=True,
                ),
            ),
        ],
        warnings=[],
        analyzed_packages=3,
    )


@pytest.fixture()
def clean_report() -> ConflictReport:
    """A ConflictReport with no conflicts."""
    return ConflictReport(
        has_conflicts=False,
        conflicts=[],
        warnings=[],
        analyzed_packages=5,
    )


# ── Tests ────────────────────────────────────────────────────


class TestConflictsFound:
    """depwhy SOURCE with conflicts present."""

    def test_exit_code_1(self, conflict_report: ConflictReport, tmp_path: object) -> None:
        """Exit code 1 when conflicts are detected."""
        req_file = _write_req(tmp_path, "requests==2.28.0\nurllib3==2.0.0\n")  # type: ignore[arg-type]
        with patch("depwhy.cli.analyze", return_value=conflict_report):
            result = runner.invoke(app, [str(req_file)])
        assert result.exit_code == 1

    def test_output_contains_conflict(
        self, conflict_report: ConflictReport, tmp_path: object
    ) -> None:
        """Output mentions 'conflict' when conflicts exist."""
        req_file = _write_req(tmp_path, "requests==2.28.0\n")  # type: ignore[arg-type]
        with patch("depwhy.cli.analyze", return_value=conflict_report):
            result = runner.invoke(app, [str(req_file)])
        assert "conflict" in result.output.lower()


class TestNoConflicts:
    """depwhy SOURCE with a clean report."""

    def test_exit_code_0(self, clean_report: ConflictReport, tmp_path: object) -> None:
        """Exit code 0 when no conflicts detected."""
        req_file = _write_req(tmp_path, "requests>=2.28.0\n")  # type: ignore[arg-type]
        with patch("depwhy.cli.analyze", return_value=clean_report):
            result = runner.invoke(app, [str(req_file)])
        assert result.exit_code == 0

    def test_output_no_conflicts(self, clean_report: ConflictReport, tmp_path: object) -> None:
        """Output says 'No conflicts' when the report is clean."""
        req_file = _write_req(tmp_path, "requests>=2.28.0\n")  # type: ignore[arg-type]
        with patch("depwhy.cli.analyze", return_value=clean_report):
            result = runner.invoke(app, [str(req_file)])
        assert "no conflicts" in result.output.lower()


class TestJsonOutput:
    """depwhy --json SOURCE."""

    def test_json_valid(self, conflict_report: ConflictReport, tmp_path: object) -> None:
        """--json produces valid JSON."""
        req_file = _write_req(tmp_path, "requests==2.28.0\n")  # type: ignore[arg-type]
        with patch("depwhy.cli.analyze", return_value=conflict_report):
            result = runner.invoke(app, ["--json", str(req_file)])
        payload = json.loads(result.output)
        assert payload["has_conflicts"] is True

    def test_json_exit_code(self, conflict_report: ConflictReport, tmp_path: object) -> None:
        """--json still returns exit code 1 when conflicts exist."""
        req_file = _write_req(tmp_path, "requests==2.28.0\n")  # type: ignore[arg-type]
        with patch("depwhy.cli.analyze", return_value=conflict_report):
            result = runner.invoke(app, ["--json", str(req_file)])
        assert result.exit_code == 1

    def test_json_clean(self, clean_report: ConflictReport, tmp_path: object) -> None:
        """--json with no conflicts exits 0 and reports has_conflicts=false."""
        req_file = _write_req(tmp_path, "requests>=2.28.0\n")  # type: ignore[arg-type]
        with patch("depwhy.cli.analyze", return_value=clean_report):
            result = runner.invoke(app, ["--json", str(req_file)])
        assert result.exit_code == 0
        payload = json.loads(result.output)
        assert payload["has_conflicts"] is False


class TestQuietMode:
    """depwhy --quiet SOURCE."""

    def test_quiet_with_conflicts(self, conflict_report: ConflictReport, tmp_path: object) -> None:
        """--quiet suppresses output but exits 1 on conflicts."""
        req_file = _write_req(tmp_path, "requests==2.28.0\n")  # type: ignore[arg-type]
        with patch("depwhy.cli.analyze", return_value=conflict_report):
            result = runner.invoke(app, ["--quiet", str(req_file)])
        assert result.exit_code == 1
        assert result.output == ""

    def test_quiet_no_conflicts(self, clean_report: ConflictReport, tmp_path: object) -> None:
        """--quiet suppresses output and exits 0 when clean."""
        req_file = _write_req(tmp_path, "requests>=2.28.0\n")  # type: ignore[arg-type]
        with patch("depwhy.cli.analyze", return_value=clean_report):
            result = runner.invoke(app, ["--quiet", str(req_file)])
        assert result.exit_code == 0
        assert result.output == ""


class TestVersion:
    """depwhy --version."""

    def test_version_output(self) -> None:
        """--version prints version string and exits 0."""
        result = runner.invoke(app, ["--version"])
        assert result.exit_code == 0
        assert "depwhy" in result.output
        assert "0.1.0" in result.output


class TestErrorCases:
    """Invalid invocations that should exit 2."""

    def test_nonexistent_file(self) -> None:
        """Nonexistent file exits 2."""
        result = runner.invoke(app, ["nonexistent_file.txt"])
        assert result.exit_code == 2

    def test_no_args_no_env(self) -> None:
        """No SOURCE and no --env exits 2."""
        result = runner.invoke(app, [])
        assert result.exit_code == 2

    def test_both_source_and_env(self, tmp_path: object) -> None:
        """Both SOURCE and --env exits 2."""
        req_file = _write_req(tmp_path, "requests>=2.28.0\n")  # type: ignore[arg-type]
        result = runner.invoke(app, ["--env", str(req_file)])
        assert result.exit_code == 2

    def test_analyze_exception(self, tmp_path: object) -> None:
        """Unexpected exception from analyze() exits 2."""
        req_file = _write_req(tmp_path, "requests>=2.28.0\n")  # type: ignore[arg-type]
        with patch("depwhy.cli.analyze", side_effect=ValueError("bad input")):
            result = runner.invoke(app, [str(req_file)])
        assert result.exit_code == 2


class TestNoColor:
    """depwhy --no-color SOURCE."""

    def test_no_color_runs(self, clean_report: ConflictReport, tmp_path: object) -> None:
        """--no-color exits correctly and produces output."""
        req_file = _write_req(tmp_path, "requests>=2.28.0\n")  # type: ignore[arg-type]
        with patch("depwhy.cli.analyze", return_value=clean_report):
            result = runner.invoke(app, ["--no-color", str(req_file)])
        assert result.exit_code == 0
        assert result.output != ""


# ── Helpers ──────────────────────────────────────────────────


def _write_req(tmp_path: object, content: str) -> object:
    """Write a temporary requirements.txt and return its path."""
    from pathlib import Path

    p = Path(str(tmp_path)) / "requirements.txt"
    p.write_text(content)
    return p
