from collections.abc import Generator
from datetime import datetime
import os
from pathlib import Path
from textwrap import dedent
from typing import Callable
from zoneinfo import ZoneInfo

from freezegun import freeze_time
from httpx import Request, RequestError
import pytest
from pytest_httpx import HTTPXMock
from typer.testing import CliRunner

from uv_secure import app


runner = CliRunner()


@pytest.fixture
def temp_uv_requirements_txt_file(tmp_path: Path) -> Path:
    uv_requirements_txt_path = tmp_path / "requirements.txt"
    requirements_txt_data = """
        # This file was autogenerated by uv via the following command:
        #    uv pip compile pyproject.toml -o requirements.txt
        example-package==1.0.0
    """
    uv_requirements_txt_path.write_text(dedent(requirements_txt_data).strip())
    return uv_requirements_txt_path


@pytest.fixture
def temp_non_uv_requirements_txt_file(tmp_path: Path) -> Path:
    uv_requirements_txt_path = tmp_path / "requirements.txt"
    requirements_txt_data = """
        example-package==1.0.0
    """
    uv_requirements_txt_path.write_text(dedent(requirements_txt_data).strip())
    return uv_requirements_txt_path


@pytest.fixture
def temp_uv_secure_toml_file_ignored_vulnerability(tmp_path: Path) -> Path:
    uv_secure_toml_path = tmp_path / "uv-secure.toml"
    uv_lock_data = """
        ignore_vulnerabilities = ["VULN-123"]
    """
    uv_secure_toml_path.write_text(dedent(uv_lock_data).strip())
    return uv_secure_toml_path


@pytest.fixture
def temp_uv_secure_toml_file_all_columns_enabled(tmp_path: Path) -> Path:
    uv_secure_toml_path = tmp_path / "uv-secure.toml"
    uv_lock_data = """
        aliases = true
        desc = true
    """
    uv_secure_toml_path.write_text(dedent(uv_lock_data).strip())
    return uv_secure_toml_path


@pytest.fixture
def temp_uv_secure_toml_file_all_columns_and_maintenance_issues_enabled(
    tmp_path: Path,
) -> Path:
    uv_secure_toml_path = tmp_path / "uv-secure.toml"
    uv_lock_data = """
        aliases = true
        desc = true

        [maintainability_criteria]
        max_package_age = "P1000D"
        forbid_yanked = true
    """
    uv_secure_toml_path.write_text(dedent(uv_lock_data).strip())
    return uv_secure_toml_path


@pytest.fixture
def temp_uv_secure_toml_file_custom_caching(tmp_path: Path) -> Path:
    uv_secure_toml_path = tmp_path / "uv-secure.toml"
    uv_lock_data = f"""
        aliases = true
        desc = true

        [cache_settings]
        cache_path = "{(tmp_path / ".uv-secure").as_posix()}"
        ttl_seconds = 60.0
    """
    uv_secure_toml_path.write_text(dedent(uv_lock_data).strip())
    return uv_secure_toml_path


@pytest.fixture
def temp_dot_uv_secure_toml_file(tmp_path: Path) -> Path:
    uv_secure_toml_path = tmp_path / ".uv-secure.toml"
    uv_lock_data = ""
    uv_secure_toml_path.write_text(uv_lock_data)
    return uv_secure_toml_path


@pytest.fixture
def temp_nested_uv_secure_toml_file_ignored_vulnerability(tmp_path: Path) -> Path:
    nested_uv_lock_path = tmp_path / "nested_project"
    uv_secure_toml_path = nested_uv_lock_path / "uv-secure.toml"
    uv_lock_data = """
    ignore_vulnerabilities = ["VULN-123"]
    """
    uv_secure_toml_path.write_text(dedent(uv_lock_data).strip())
    return uv_secure_toml_path


@pytest.fixture
def temp_pyproject_toml_file(tmp_path: Path) -> Path:
    pyproject_toml_path = tmp_path / "pyproject.toml"
    uv_lock_data = """
    [tool.uv-secure]
    """
    pyproject_toml_path.write_text(dedent(uv_lock_data).strip())
    return pyproject_toml_path


@pytest.fixture
def temp_pyproject_toml_file_extra_columns_enabled(tmp_path: Path) -> Path:
    pyproject_toml_path = tmp_path / "pyproject.toml"
    uv_lock_data = """
    [tool.uv-secure]
    aliases = true
    desc = true
    """
    pyproject_toml_path.write_text(dedent(uv_lock_data).strip())
    return pyproject_toml_path


@pytest.fixture
def temp_pyproject_toml_file_ignored_vulnerability(tmp_path: Path) -> Path:
    pyproject_toml_path = tmp_path / "pyproject.toml"
    uv_lock_data = """
    [tool.uv-secure]
    ignore_vulnerabilities = ["VULN-123"]
    """
    pyproject_toml_path.write_text(dedent(uv_lock_data).strip())
    return pyproject_toml_path


@pytest.fixture
def temp_nested_pyproject_toml_file_no_config(tmp_path: Path) -> Path:
    pyproject_toml_path = tmp_path / "nested_project" / "pyproject.toml"
    uv_lock_data = ""
    pyproject_toml_path.write_text(uv_lock_data)
    return pyproject_toml_path


@pytest.fixture
def temp_nested_uv_lock_file(tmp_path: Path) -> Path:
    nested_uv_lock_path = tmp_path / "nested_project"
    nested_uv_lock_path.mkdir()
    uv_lock_path = nested_uv_lock_path / "uv.lock"
    uv_lock_data = """
    [[package]]
    name = "example-package"
    version = "2.0.0"
    source = { registry = "https://pypi.org/simple" }
    """
    uv_lock_path.write_text(dedent(uv_lock_data).strip())
    return uv_lock_path


@pytest.fixture
def temp_double_nested_uv_lock_file(tmp_path: Path) -> Path:
    double_nested_uv_lock_path = tmp_path / "nested_project" / "double_nested_project"
    double_nested_uv_lock_path.mkdir(parents=True)
    uv_lock_path = double_nested_uv_lock_path / "uv.lock"
    uv_lock_data = """
    [[package]]
    name = "example-package"
    version = "2.0.0"
    source = { registry = "https://pypi.org/simple" }
    """
    uv_lock_path.write_text(dedent(uv_lock_data).strip())
    return uv_lock_path


@pytest.fixture
def no_vulnerabilities_response(httpx_mock: HTTPXMock) -> HTTPXMock:
    httpx_mock.add_response(
        url="https://pypi.org/pypi/example-package/1.0.0/json",
        json={
            "info": {
                "author_email": "example@example.com",
                "classifiers": [],
                "description": "A minimal package",
                "description_content_type": "text/plain",
                "downloads": {"last_day": None, "last_month": None, "last_week": None},
                "name": "example-package",
                "project_urls": {},
                "provides_extra": [],
                "release_url": "https://pypi.org/project/example-package/1.0.0/",
                "requires_python": ">=3.9",
                "summary": "A minimal package example",
                "version": "1.0.0",
                "yanked": False,
            },
            "last_serial": 1,
            "urls": [],
            "vulnerabilities": [],
        },
    )
    return httpx_mock


@pytest.fixture
def one_vulnerability_response(httpx_mock: HTTPXMock) -> HTTPXMock:
    httpx_mock.add_response(
        url="https://pypi.org/pypi/example-package/1.0.0/json",
        json={
            "info": {
                "author_email": "example@example.com",
                "classifiers": [],
                "description": "A minimal package",
                "description_content_type": "text/plain",
                "downloads": {"last_day": None, "last_month": None, "last_week": None},
                "name": "example-package",
                "project_urls": {},
                "provides_extra": [],
                "release_url": "https://pypi.org/project/example-package/1.0.0/",
                "requires_python": ">=3.9",
                "summary": "A minimal package example",
                "version": "1.0.0",
                "yanked": False,
            },
            "last_serial": 1,
            "urls": [],
            "vulnerabilities": [
                {
                    "aliases": ["CVE-2024-12345"],
                    "id": "VULN-123",
                    "details": "A critical vulnerability in example-package.",
                    "fixed_in": ["1.0.1"],
                    "link": "https://example.com/vuln-123",
                }
            ],
        },
    )
    return httpx_mock


@pytest.fixture
def old_yanked_package_response(httpx_mock: HTTPXMock) -> HTTPXMock:
    httpx_mock.add_response(
        url="https://pypi.org/pypi/example-package/1.0.0/json",
        json={
            "info": {
                "author_email": "example@example.com",
                "classifiers": [],
                "description": "A minimal package",
                "description_content_type": "text/plain",
                "downloads": {"last_day": None, "last_month": None, "last_week": None},
                "name": "example-package",
                "project_urls": {},
                "provides_extra": [],
                "release_url": "https://pypi.org/project/example-package/1.0.0/",
                "requires_python": ">=3.9",
                "summary": "A minimal package example",
                "version": "1.0.0",
                "yanked": True,
                "yanked_reason": "Broken API",
            },
            "last_serial": 1,
            "urls": [
                {
                    "comment_text": "",
                    "digests": {
                        "blake2b_256": (
                            "0bf785273299ab57117850cc0a936c64151171fac4da49bc6fba0dad98"
                            "4a7c5f"
                        ),
                        "md5": "8626f021f29631950dfad7b4c6435fc4",
                        "sha256": (
                            "8a3df80e2b2378aef598a83c1392efd47967afec4242021a0b06b4c7cb"
                            "c61a92"
                        ),
                    },
                    "downloads": -1,
                    "filename": "example-package-1.0.0-py3-none-any.whl",
                    "has_sig": False,
                    "md5_digest": "8626f021f29631950dfad7b4c6435fc4",
                    "packagetype": "bdist_wheel",
                    "python_version": "py3",
                    "requires_python": ">=3.7",
                    "size": 15662,
                    "upload_time": "2021-01-19T23:44:28",
                    "upload_time_iso_8601": "2021-01-19T23:44:28.833863Z",
                    "url": (
                        "https://files.pythonhosted.org/packages/0b/f7/"
                        "85273299ab57117850cc0a936c64151171fac4da49bc6fba0dad984a7c5f/"
                        "example-package-1.0.0-py3-none-any.whl"
                    ),
                    "yanked": True,
                    "yanked_reason": "Broken API",
                }
            ],
            "vulnerabilities": [],
        },
    )
    return httpx_mock


@pytest.fixture
def yanked_package_no_reason_given_response(httpx_mock: HTTPXMock) -> HTTPXMock:
    httpx_mock.add_response(
        url="https://pypi.org/pypi/example-package/1.0.0/json",
        json={
            "info": {
                "author_email": "example@example.com",
                "classifiers": [],
                "description": "A minimal package",
                "description_content_type": "text/plain",
                "downloads": {"last_day": None, "last_month": None, "last_week": None},
                "name": "example-package",
                "project_urls": {},
                "provides_extra": [],
                "release_url": "https://pypi.org/project/example-package/1.0.0/",
                "requires_python": ">=3.9",
                "summary": "A minimal package example",
                "version": "1.0.0",
                "yanked": True,
            },
            "last_serial": 1,
            "urls": [
                {
                    "comment_text": "",
                    "digests": {
                        "blake2b_256": (
                            "0bf785273299ab57117850cc0a936c64151171fac4da49bc6fba0dad98"
                            "4a7c5f"
                        ),
                        "md5": "8626f021f29631950dfad7b4c6435fc4",
                        "sha256": (
                            "8a3df80e2b2378aef598a83c1392efd47967afec4242021a0b06b4c7cb"
                            "c61a92"
                        ),
                    },
                    "downloads": -1,
                    "filename": "example-package-1.0.0-py3-none-any.whl",
                    "has_sig": False,
                    "md5_digest": "8626f021f29631950dfad7b4c6435fc4",
                    "packagetype": "bdist_wheel",
                    "python_version": "py3",
                    "requires_python": ">=3.7",
                    "size": 15662,
                    "upload_time": "2024-01-19T23:44:28",
                    "upload_time_iso_8601": "2024-01-19T23:44:28.833863Z",
                    "url": (
                        "https://files.pythonhosted.org/packages/0b/f7/"
                        "85273299ab57117850cc0a936c64151171fac4da49bc6fba0dad984a7c5f/"
                        "example-package-1.0.0-py3-none-any.whl"
                    ),
                    "yanked": True,
                }
            ],
            "vulnerabilities": [],
        },
    )
    return httpx_mock


@pytest.fixture
def old_yanked_package_with_vulnerability_response(httpx_mock: HTTPXMock) -> HTTPXMock:
    httpx_mock.add_response(
        url="https://pypi.org/pypi/example-package/1.0.0/json",
        json={
            "info": {
                "author_email": "example@example.com",
                "classifiers": [],
                "description": "A minimal package",
                "description_content_type": "text/plain",
                "downloads": {"last_day": None, "last_month": None, "last_week": None},
                "name": "example-package",
                "project_urls": {},
                "provides_extra": [],
                "release_url": "https://pypi.org/project/example-package/1.0.0/",
                "requires_python": ">=3.9",
                "summary": "A minimal package example",
                "version": "1.0.0",
                "yanked": True,
                "yanked_reason": "Broken API",
            },
            "last_serial": 1,
            "urls": [
                {
                    "comment_text": "",
                    "digests": {
                        "blake2b_256": (
                            "0bf785273299ab57117850cc0a936c64151171fac4da49bc6fba0dad98"
                            "4a7c5f"
                        ),
                        "md5": "8626f021f29631950dfad7b4c6435fc4",
                        "sha256": (
                            "8a3df80e2b2378aef598a83c1392efd47967afec4242021a0b06b4c7cb"
                            "c61a92"
                        ),
                    },
                    "downloads": -1,
                    "filename": "example-package-1.0.0-py3-none-any.whl",
                    "has_sig": False,
                    "md5_digest": "8626f021f29631950dfad7b4c6435fc4",
                    "packagetype": "bdist_wheel",
                    "python_version": "py3",
                    "requires_python": ">=3.7",
                    "size": 15662,
                    "upload_time": "2021-01-19T23:44:28",
                    "upload_time_iso_8601": "2021-01-19T23:44:28.833863Z",
                    "url": (
                        "https://files.pythonhosted.org/packages/0b/f7/"
                        "85273299ab57117850cc0a936c64151171fac4da49bc6fba0dad984a7c5f/"
                        "example-package-1.0.0-py3-none-any.whl"
                    ),
                    "yanked": True,
                    "yanked_reason": "Broken API",
                }
            ],
            "vulnerabilities": [
                {
                    "aliases": ["CVE-2024-12345"],
                    "id": "VULN-123",
                    "details": "A critical vulnerability in example-package.",
                    "fixed_in": ["1.0.1"],
                    "link": "https://example.com/vuln-123",
                }
            ],
        },
    )
    return httpx_mock


@pytest.fixture
def temp_uv_lock_file_jinja2(tmp_path: Path) -> Path:
    """Fixture to create a temporary uv.lock file with a single jinja2 dependency."""
    uv_lock_path = tmp_path / "uv.lock"
    uv_lock_data = """
    [[package]]
    name = "jinja2"
    version = "3.1.4"
    source = { registry = "https://pypi.org/simple" }
    """
    uv_lock_path.write_text(dedent(uv_lock_data).strip())
    return uv_lock_path


@pytest.fixture
def jinja2_two_longer_vulnerability_responses(httpx_mock: HTTPXMock) -> HTTPXMock:
    httpx_mock.add_response(
        url="https://pypi.org/pypi/jinja2/3.1.4/json",
        json={
            "info": {
                "author_email": "example@example.com",
                "classifiers": [],
                "description": "Jinja2 templating",
                "description_content_type": "text/plain",
                "downloads": {"last_day": None, "last_month": None, "last_week": None},
                "name": "jinja2",
                "project_urls": {},
                "provides_extra": [],
                "release_url": "https://pypi.org/project/jinja2/3.1.4/",
                "requires_python": ">=3.9",
                "summary": "Jinja2 templating",
                "version": "3.1.4",
                "yanked": False,
            },
            "last_serial": 1,
            "urls": [],
            "vulnerabilities": [
                {
                    "aliases": ["CVE-2024-56326"],
                    "details": (
                        "An oversight in how the Jinja sandboxed environment detects "
                        "calls to `str.format` allows an attacker that controls the "
                        "content of a template to execute arbitrary Python code.\n\nTo "
                        "exploit the vulnerability, an attacker needs to control the "
                        "content of a template. Whether that is the case depends on "
                        "the type of application using Jinja. This vulnerability "
                        "impacts users of applications which execute untrusted "
                        "templates.\n\nJinja's sandbox does catch calls to "
                        "`str.format` and ensures they don't escape the sandbox. "
                        "However, it's possible to store a reference to a malicious "
                        "string's `format` method, then pass that to a filter that "
                        "calls it. No such filters are built-in to Jinja, but could be "
                        "present through custom filters in an application. After the "
                        "fix, such indirect calls are also handled by the sandbox."
                    ),
                    "fixed_in": ["3.1.5"],
                    "id": "GHSA-q2x7-8rv6-6q7h",
                    "link": "https://osv.dev/vulnerability/GHSA-q2x7-8rv6-6q7h",
                    "source": "osv",
                    "summary": None,
                    "withdrawn": None,
                },
                {
                    "aliases": ["CVE-2024-56201"],
                    "details": (
                        "A bug in the Jinja compiler allows an attacker that controls "
                        "both the content and filename of a template to execute "
                        "arbitrary Python code, regardless of if Jinja's sandbox is "
                        "used.\n\nTo exploit the vulnerability, an attacker needs to "
                        "control both the filename and the contents of a template. "
                        "Whether that is the case depends on the type of application "
                        "using Jinja. This vulnerability impacts users of applications "
                        "which execute untrusted templates where the template author "
                        "can also choose the template filename."
                    ),
                    "fixed_in": ["3.1.5"],
                    "id": "GHSA-gmj6-6f8f-6699",
                    "link": "https://osv.dev/vulnerability/GHSA-gmj6-6f8f-6699",
                    "source": "osv",
                    "summary": None,
                    "withdrawn": None,
                },
            ],
        },
    )
    return httpx_mock


@pytest.fixture
def one_vulnerability_response_v2(httpx_mock: HTTPXMock) -> HTTPXMock:
    httpx_mock.add_response(
        url="https://pypi.org/pypi/example-package/2.0.0/json",
        json={
            "info": {
                "author_email": "example@example.com",
                "classifiers": [],
                "description": "A minimal package",
                "description_content_type": "text/plain",
                "downloads": {"last_day": None, "last_month": None, "last_week": None},
                "name": "example-package",
                "project_urls": {},
                "provides_extra": [],
                "release_url": "https://pypi.org/project/example-package/2.0.0/",
                "requires_python": ">=3.9",
                "summary": "A minimal package example",
                "version": "2.0.0",
                "yanked": False,
            },
            "last_serial": 1,
            "urls": [],
            "vulnerabilities": [
                {
                    "id": "VULN-123",
                    "details": "A critical vulnerability in example-package.",
                    "fixed_in": ["2.0.1"],
                    "link": "https://example.com/vuln-123",
                }
            ],
        },
    )
    return httpx_mock


@pytest.fixture
def package_version_not_found_response(httpx_mock: HTTPXMock) -> HTTPXMock:
    httpx_mock.add_response(
        url="https://pypi.org/pypi/example-package/1.0.0/json", status_code=404
    )
    return httpx_mock


@pytest.fixture
def missing_vulnerability_response(httpx_mock: HTTPXMock) -> HTTPXMock:
    httpx_mock.add_exception(
        RequestError(
            "Request failed",
            request=Request("GET", "https://pypi.org/pypi/example-package/1.0.0/json"),
        )
    )
    return httpx_mock


@pytest.fixture(scope="session", autouse=True)
def wide_console() -> Generator:
    mp = pytest.MonkeyPatch()
    mp.setenv("COLUMNS", "200")
    yield mp
    mp.undo()


@pytest.fixture
def set_console_width(monkeypatch: pytest.MonkeyPatch) -> Callable[[int], None]:
    def _set_width(width: int) -> None:
        monkeypatch.setenv("COLUMNS", str(width))

    return _set_width


def test_app_version() -> None:
    result = runner.invoke(app, "--version")
    assert result.exit_code == 0
    assert "uv-secure " in result.output


def test_bad_file_name() -> None:
    result = runner.invoke(app, "i_dont_exist.txt")
    assert result.exit_code == 3
    assert "Error" in result.output


def test_missing_file(tmp_path: Path) -> None:
    result = runner.invoke(app, [str(tmp_path / "uv.lock")])
    assert result.exit_code == 3
    assert "Error" in result.output


def test_non_uv_requirements_txt_file(temp_non_uv_requirements_txt_file: Path) -> None:
    result = runner.invoke(app, [str(temp_non_uv_requirements_txt_file)])

    assert result.exit_code == 0
    assert "doesn't appear to be a uv generated requirements.txt file" in result.output


def test_app_no_vulnerabilities(
    temp_uv_lock_file: Path, no_vulnerabilities_response: HTTPXMock
) -> None:
    result = runner.invoke(app, [str(temp_uv_lock_file), "--disable-cache"])

    assert result.exit_code == 0
    assert "No vulnerabilities detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "All dependencies appear safe!" in result.output


def test_app_no_vulnerabilities_requirements_txt(
    temp_uv_requirements_txt_file: Path, no_vulnerabilities_response: HTTPXMock
) -> None:
    result = runner.invoke(app, [str(temp_uv_requirements_txt_file), "--disable-cache"])

    assert result.exit_code == 0
    assert "No vulnerabilities detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "All dependencies appear safe!" in result.output


def test_app_no_vulnerabilities_requirements_txt_no_specified_path(
    tmp_path: Path,
    temp_uv_requirements_txt_file: Path,
    no_vulnerabilities_response: HTTPXMock,
) -> None:
    os.chdir(tmp_path)
    result = runner.invoke(app, "--disable-cache")

    assert result.exit_code == 0
    assert "No vulnerabilities detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "All dependencies appear safe!" in result.output


def test_app_no_vulnerabilities_relative_lock_file_path(
    tmp_path: Path, temp_uv_lock_file: Path, no_vulnerabilities_response: HTTPXMock
) -> None:
    os.chdir(tmp_path)
    result = runner.invoke(app, ["uv.lock", "--disable-cache"])

    assert result.exit_code == 0
    assert "No vulnerabilities detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "All dependencies appear safe!" in result.output


def test_app_no_vulnerabilities_relative_no_specified_path(
    tmp_path: Path, temp_uv_lock_file: Path, no_vulnerabilities_response: HTTPXMock
) -> None:
    os.chdir(tmp_path)
    result = runner.invoke(app, "--disable-cache")

    assert result.exit_code == 0
    assert "No vulnerabilities detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "All dependencies appear safe!" in result.output


@freeze_time(datetime(2025, 1, 30, tzinfo=ZoneInfo("UTC")))
def test_app_maintenance_issues_cli_args(
    temp_uv_lock_file: Path, old_yanked_package_response: HTTPXMock
) -> None:
    result = runner.invoke(
        app,
        [
            str(temp_uv_lock_file),
            "--forbid-yanked",
            "--max-age-days",
            "1000",
            "--disable-cache",
        ],
    )

    assert result.exit_code == 1
    assert "Maintenance Issues detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "Issues: 1 issue" in result.output
    assert "Maintenance Issues" in result.output
    assert "Broken API" in result.output
    assert "4 years and 11.01 days" in result.output


@freeze_time(datetime(2025, 1, 30, tzinfo=ZoneInfo("UTC")))
def test_app_yanked_no_reason_cli_args(
    temp_uv_lock_file: Path, yanked_package_no_reason_given_response: HTTPXMock
) -> None:
    result = runner.invoke(
        app, [str(temp_uv_lock_file), "--forbid-yanked", "--disable-cache"]
    )

    assert result.exit_code == 1
    assert "Maintenance Issues detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "Issues: 1 issue" in result.output
    assert "Maintenance Issues" in result.output
    assert "Unknown" in result.output
    assert "1 year and 11.01 days" in result.output


def test_app_failed_vulnerability_request(
    temp_uv_lock_file: Path, missing_vulnerability_response: HTTPXMock
) -> None:
    result = runner.invoke(app, [str(temp_uv_lock_file), "--disable-cache"])

    assert result.exit_code == 0
    assert (
        "Error: name='example-package' version='1.0.0' raised exception: Request failed"
    ) in result.output
    assert "No vulnerabilities detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "All dependencies appear safe!" in result.output


def test_app_package_not_found(
    temp_uv_lock_file: Path, package_version_not_found_response: HTTPXMock
) -> None:
    result = runner.invoke(app, [str(temp_uv_lock_file), "--disable-cache"])

    assert result.exit_code == 0
    assert (
        "Error: name='example-package' version='1.0.0' raised exception: Client "
        "error '404 Not Found' for url "
        "'https://pypi.org/pypi/example-package/1.0.0/json'"
    ) in result.output
    assert "No vulnerabilities detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "All dependencies appear safe!" in result.output


@pytest.mark.parametrize(
    "extra_cli_args",
    [
        pytest.param([], id="Default arguments"),
        pytest.param(["--aliases"], id="Add Aliases column"),
        pytest.param(["--desc"], id="Add details column"),
        pytest.param(["--aliases", "--desc"], id="Add details column"),
        pytest.param(
            ["--forbid-yanked", "--max-age-days", "1000"], id="Maintenance criteria"
        ),
    ],
)
@freeze_time(datetime(2025, 1, 30, tzinfo=ZoneInfo("UTC")))
def test_check_dependencies_with_vulnerability(
    extra_cli_args: list[str],
    temp_uv_lock_file: Path,
    one_vulnerability_response: HTTPXMock,
) -> None:
    """Test check_dependencies with a single dependency and a single vulnerability."""
    result = runner.invoke(
        app, [str(temp_uv_lock_file), *extra_cli_args, "--disable-cache"]
    )

    assert result.exit_code == 2
    assert "Vulnerabilities detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "Vulnerable: 1 vulnerability" in result.output
    assert "example-package" in result.output
    assert "1.0.0" in result.output
    assert "VULN-123" in result.output
    assert "1.0.1" in result.output
    if "--aliases" in extra_cli_args:
        assert "Aliases" in result.output
        assert "CVE-2024-12345" in result.output
    if "--desc" in extra_cli_args:
        assert "Details" in result.output
        assert "A critical vulnerability in example-package.  " in result.output


def test_check_dependencies_with_vulnerability_narrow_console_vulnerability_ids_visible(
    temp_uv_lock_file_jinja2: Path,
    jinja2_two_longer_vulnerability_responses: HTTPXMock,
    set_console_width: Callable[[int], None],
) -> None:
    """Test check_dependencies with a single dependency and a single vulnerability."""
    set_console_width(80)
    result = runner.invoke(
        app, [str(temp_uv_lock_file_jinja2), "--aliases", "--desc", "--disable-cache"]
    )

    assert result.exit_code == 2
    assert "GHSA-q2x7-8rv6-6q7h" in result.output
    assert "GHSA-gmj6-6f8f-6699" in result.output


def test_check_dependencies_with_two_longer_vulnerabilities(
    temp_uv_lock_file_jinja2: Path, jinja2_two_longer_vulnerability_responses: HTTPXMock
) -> None:
    """Test check_dependencies with a single dependency and a single vulnerability."""
    result = runner.invoke(app, [str(temp_uv_lock_file_jinja2), "--disable-cache"])

    assert result.exit_code == 2
    assert "Vulnerabilities detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "Vulnerable: 2 vulnerabilities" in result.output
    assert result.output.count("jinja2") == 2
    assert result.output.count("3.1.4") == 2
    assert result.output.count("3.1.5") == 2
    assert "GHSA-q2x7-8rv6-6q7h" in result.output
    assert "GHSA-gmj6-6f8f-6699" in result.output


def test_app_with_arg_ignored_vulnerability(
    temp_uv_lock_file: Path, one_vulnerability_response: HTTPXMock
) -> None:
    result = runner.invoke(
        app, [str(temp_uv_lock_file), "--ignore", "VULN-123", "--disable-cache"]
    )

    assert result.exit_code == 0
    assert "No vulnerabilities detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "All dependencies appear safe!" in result.output


def test_check_dependencies_with_vulnerability_pyproject_all_columns_configured(
    temp_uv_lock_file: Path,
    temp_pyproject_toml_file_extra_columns_enabled: Path,
    one_vulnerability_response: HTTPXMock,
) -> None:
    """Test check_dependencies with a single dependency and a single vulnerability."""
    result = runner.invoke(app, [str(temp_uv_lock_file), "--disable-cache"])

    assert result.exit_code == 2
    assert "Vulnerabilities detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "Vulnerable: 1 vulnerability" in result.output
    assert "example-package" in result.output
    assert "1.0.0" in result.output
    assert "VULN-123" in result.output
    assert "1.0.1" in result.output
    assert "Aliases" in result.output
    assert "CVE-2024-12345" in result.output
    assert "Details" in result.output
    assert "A critical vulnerability in example-package.  " in result.output


def test_check_dependencies_with_vulnerability_uv_secure_all_columns_configured(
    temp_uv_lock_file: Path,
    temp_uv_secure_toml_file_all_columns_enabled: Path,
    one_vulnerability_response: HTTPXMock,
) -> None:
    result = runner.invoke(app, [str(temp_uv_lock_file), "--disable-cache"])

    assert result.exit_code == 2
    assert "Vulnerabilities detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "Vulnerable: 1 vulnerability" in result.output
    assert "example-package" in result.output
    assert "1.0.0" in result.output
    assert "VULN-123" in result.output
    assert "1.0.1" in result.output
    assert "Aliases" in result.output
    assert "CVE-2024-12345" in result.output
    assert "Details" in result.output
    assert "A critical vulnerability in example-package.  " in result.output


@freeze_time(datetime(2025, 1, 30, tzinfo=ZoneInfo("UTC")))
def test_check_dependencies_with_vulnerability_and_maintenance_issues_uv_secure(
    temp_uv_lock_file: Path,
    temp_uv_secure_toml_file_all_columns_and_maintenance_issues_enabled: Path,
    old_yanked_package_with_vulnerability_response: HTTPXMock,
) -> None:
    result = runner.invoke(app, [str(temp_uv_lock_file), "--disable-cache"])

    assert result.exit_code == 2
    assert "Vulnerabilities detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "Vulnerable: 1 vulnerability" in result.output
    assert "example-package" in result.output
    assert "1.0.0" in result.output
    assert "VULN-123" in result.output
    assert "1.0.1" in result.output
    assert "Aliases" in result.output
    assert "CVE-2024-12345" in result.output
    assert "Details" in result.output
    assert "A critical vulnerability in example-package." in result.output
    assert "Maintenance Issues detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "Issues: 1 issue" in result.output
    assert "Maintenance Issues" in result.output
    assert "Broken API" in result.output
    assert "4 years and 11.01 days" in result.output


def test_check_dependencies_with_custom_caching(
    temp_uv_lock_file: Path,
    temp_uv_secure_toml_file_custom_caching: Path,
    no_vulnerabilities_response: HTTPXMock,
) -> None:
    result = runner.invoke(app, [str(temp_uv_lock_file), "--disable-cache"])
    assert result.exit_code == 0
    assert "No vulnerabilities detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "All dependencies appear safe!" in result.output


def test_check_dependencies_with_vulnerability_pyproject_toml_cli_argument_override(
    temp_uv_lock_file: Path,
    temp_pyproject_toml_file_ignored_vulnerability: Path,
    one_vulnerability_response: HTTPXMock,
) -> None:
    result = runner.invoke(
        app,
        [str(temp_uv_lock_file), "--ignore", "VULN-NOT-HERE", "--aliases", "--desc"],
        "--disable-cache",
    )

    assert "Vulnerabilities detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "Vulnerable: 1 vulnerability" in result.output
    assert "example-package" in result.output
    assert "1.0.0" in result.output
    assert "VULN-123" in result.output
    assert "1.0.1" in result.output
    assert "Aliases" in result.output
    assert "CVE-2024-12345" in result.output
    assert "Details" in result.output
    assert "A critical vulnerability in example-package.  " in result.output


def test_app_with_uv_secure_toml_ignored_vulnerability(
    temp_uv_lock_file: Path,
    temp_uv_secure_toml_file_ignored_vulnerability: Path,
    one_vulnerability_response: HTTPXMock,
) -> None:
    result = runner.invoke(
        app,
        [
            str(temp_uv_lock_file),
            "--config",
            temp_uv_secure_toml_file_ignored_vulnerability,
        ],
        "--disable-cache",
    )

    assert result.exit_code == 0
    assert "No vulnerabilities detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "All dependencies appear safe!" in result.output


def test_app_with_pyproject_toml_ignored_vulnerability(
    temp_uv_lock_file: Path,
    temp_pyproject_toml_file_ignored_vulnerability: Path,
    one_vulnerability_response: HTTPXMock,
) -> None:
    result = runner.invoke(
        app,
        [
            str(temp_uv_lock_file),
            "--config",
            temp_pyproject_toml_file_ignored_vulnerability,
        ],
        "--disable-cache",
    )

    assert result.exit_code == 0
    assert "No vulnerabilities detected!" in result.output
    assert "Checked: 1 dependency" in result.output
    assert "All dependencies appear safe!" in result.output


def test_app_multiple_lock_files_no_vulnerabilities(
    temp_uv_lock_file: Path, temp_nested_uv_lock_file: Path, httpx_mock: HTTPXMock
) -> None:
    httpx_mock.add_response(
        url="https://pypi.org/pypi/example-package/1.0.0/json",
        json={"vulnerabilities": []},
    )
    httpx_mock.add_response(
        url="https://pypi.org/pypi/example-package/2.0.0/json",
        json={"vulnerabilities": []},
    )

    result = runner.invoke(
        app, [str(temp_uv_lock_file), str(temp_nested_uv_lock_file), "--disable-cache"]
    )

    assert result.exit_code == 0
    assert result.output.count("No vulnerabilities detected!") == 2
    assert result.output.count("Checked: 1 dependency") == 2
    assert result.output.count("All dependencies appear safe!") == 2
    assert result.output.count("nested_project") == 1


def test_app_multiple_lock_files_one_vulnerabilities(
    temp_uv_lock_file: Path,
    temp_nested_uv_lock_file: Path,
    no_vulnerabilities_response: HTTPXMock,
    one_vulnerability_response_v2: HTTPXMock,
) -> None:
    result = runner.invoke(
        app, [str(temp_uv_lock_file), str(temp_nested_uv_lock_file), "--disable-cache"]
    )
    assert result.exit_code == 2
    assert result.output.count("No vulnerabilities detected!") == 1
    assert result.output.count("Vulnerabilities detected!") == 1


def test_app_multiple_lock_files_one_nested_ignored_vulnerability(
    tmp_path: Path,
    temp_uv_lock_file: Path,
    temp_nested_uv_lock_file: Path,
    temp_dot_uv_secure_toml_file: Path,
    temp_nested_uv_secure_toml_file_ignored_vulnerability: Path,
    no_vulnerabilities_response: HTTPXMock,
    one_vulnerability_response_v2: HTTPXMock,
) -> None:
    result = runner.invoke(app, [str(tmp_path), "--disable-cache"])

    assert result.exit_code == 0
    assert result.output.count("No vulnerabilities detected!") == 2
    assert result.output.count("Checked: 1 dependency") == 2
    assert result.output.count("All dependencies appear safe!") == 2
    assert result.output.count("nested_project") == 1


def test_app_multiple_lock_files_no_root_config_one_nested_ignored_vulnerability(
    tmp_path: Path,
    temp_uv_lock_file: Path,
    temp_double_nested_uv_lock_file: Path,
    temp_nested_uv_secure_toml_file_ignored_vulnerability: Path,
    no_vulnerabilities_response: HTTPXMock,
    one_vulnerability_response_v2: HTTPXMock,
) -> None:
    result = runner.invoke(app, [str(tmp_path), "--disable-cache"])

    assert result.exit_code == 0
    assert result.output.count("No vulnerabilities detected!") == 2
    assert result.output.count("Checked: 1 dependency") == 2
    assert result.output.count("All dependencies appear safe!") == 2
    assert result.output.count("nested_project") == 2


def test_app_multiple_lock_files_one_nested_ignored_vulnerability_pass_lock_files(
    tmp_path: Path,
    temp_uv_lock_file: Path,
    temp_double_nested_uv_lock_file: Path,
    temp_nested_uv_secure_toml_file_ignored_vulnerability: Path,
    no_vulnerabilities_response: HTTPXMock,
    one_vulnerability_response_v2: HTTPXMock,
) -> None:
    result = runner.invoke(
        app,
        [str(temp_uv_lock_file), str(temp_double_nested_uv_lock_file)],
        "--disable-cache",
    )

    assert result.exit_code == 0
    assert result.output.count("No vulnerabilities detected!") == 2
    assert result.output.count("Checked: 1 dependency") == 2
    assert result.output.count("All dependencies appear safe!") == 2
    assert result.output.count("nested_project") == 2


def test_app_multiple_lock_files_one_vulnerabilities_ignored_nested_pyproject_toml(
    temp_uv_lock_file: Path,
    temp_nested_uv_lock_file: Path,
    temp_pyproject_toml_file: Path,
    temp_nested_pyproject_toml_file_no_config: Path,
    no_vulnerabilities_response: HTTPXMock,
    one_vulnerability_response_v2: HTTPXMock,
) -> None:
    result = runner.invoke(
        app, [str(temp_uv_lock_file), str(temp_nested_uv_lock_file), "--disable-cache"]
    )
    assert result.exit_code == 2
    assert result.output.count("No vulnerabilities detected!") == 1
    assert result.output.count("Vulnerabilities detected!") == 1
