"""Tests for Enhanced Dependency Vulnerability Scanner (v2.5.0).

Tests for new features: deprecated packages, known-vulnerable versions,
typosquatting detection, license compliance, and scan_dependencies().
"""

from __future__ import annotations

import json

import pytest

from mcpaudit.dep_vuln import (
    DepFinding,
    DependencyReport,
    DeprecatedPackage,
    LicenseInfo,
    LicenseRisk,
    VulnerableVersion,
    _check_typosquat,
    _parse_input,
    _version_matches,
    classify_license,
    format_dep_report_json,
    format_dep_report_text,
    get_deprecated_packages,
    get_typosquat_targets,
    get_vulnerable_versions,
    scan_dependencies,
)


# --- Data Models ---

class TestDeprecatedPackage:
    def test_to_dict(self):
        dp = DeprecatedPackage("nose", "pytest", "Unmaintained")
        d = dp.to_dict()
        assert d["name"] == "nose"
        assert d["replacement"] == "pytest"
        assert d["reason"] == "Unmaintained"

    def test_fields(self):
        dp = DeprecatedPackage("pep8", "pycodestyle", "Renamed")
        assert dp.name == "pep8"
        assert dp.replacement == "pycodestyle"


class TestVulnerableVersion:
    def test_to_dict(self):
        v = VulnerableVersion("flask", ["0."], "CVE-2018-1000656", "Input validation", "MEDIUM")
        d = v.to_dict()
        assert d["package"] == "flask"
        assert d["cve"] == "CVE-2018-1000656"
        assert "0." in d["affected_versions"]

    def test_fields(self):
        v = VulnerableVersion("django", ["1."], "CVE-X", "Test", "HIGH")
        assert v.severity == "HIGH"


class TestDepFinding:
    def test_to_dict(self):
        f = DepFinding("sh", "1.0", "risky", "HIGH", "Shell wrapper", "Remove it")
        d = f.to_dict()
        assert d["package"] == "sh"
        assert d["finding_type"] == "risky"
        assert d["severity"] == "HIGH"

    def test_cve_included(self):
        f = DepFinding("flask", "0.12", "vulnerable", "HIGH", "Bug", cve="CVE-123")
        d = f.to_dict()
        assert d["cve"] == "CVE-123"

    def test_no_cve(self):
        f = DepFinding("sh", "1.0", "risky", "HIGH", "Shell")
        d = f.to_dict()
        assert "cve" not in d


class TestLicenseInfo:
    def test_to_dict(self):
        l = LicenseInfo("flask", "MIT", "ok", "")
        d = l.to_dict()
        assert d["package"] == "flask"
        assert d["license_id"] == "MIT"
        assert d["risk"] == "ok"

    def test_copyleft_note(self):
        l = LicenseInfo("pkg", "GPL-3.0", "copyleft", "Requires disclosure")
        assert l.note == "Requires disclosure"


class TestDependencyReport:
    def test_empty(self):
        r = DependencyReport()
        assert r.total_deps == 0
        assert r.total_findings == 0
        assert r.vulnerable_count == 0
        assert r.deprecated_count == 0
        assert r.typosquat_count == 0
        assert r.license_issue_count == 0
        assert r.risky_count == 0

    def test_counts(self):
        r = DependencyReport(findings=[
            DepFinding("a", "1.0", "vulnerable", "HIGH", "vuln"),
            DepFinding("b", "1.0", "deprecated", "MEDIUM", "old"),
            DepFinding("c", "1.0", "typosquat", "CRITICAL", "typo"),
            DepFinding("d", "1.0", "license", "HIGH", "gpl"),
            DepFinding("e", "1.0", "risky", "HIGH", "danger"),
        ])
        assert r.vulnerable_count == 1
        assert r.deprecated_count == 1
        assert r.typosquat_count == 1
        assert r.license_issue_count == 1
        assert r.risky_count == 1
        assert r.total_findings == 5

    def test_to_dict(self):
        r = DependencyReport(source="test", total_deps=3, risk_score=42)
        d = r.to_dict()
        assert d["source"] == "test"
        assert d["total_deps"] == 3
        assert d["risk_score"] == 42
        assert "findings" in d
        assert "licenses" in d

    def test_copyleft_licenses(self):
        r = DependencyReport(licenses=[
            LicenseInfo("a", "MIT", "ok"),
            LicenseInfo("b", "GPL-3.0", "copyleft"),
            LicenseInfo("c", "Apache-2.0", "ok"),
        ])
        copyleft = r.copyleft_licenses
        assert len(copyleft) == 1
        assert copyleft[0].package == "b"


# --- License Classification ---

class TestClassifyLicense:
    def test_mit(self):
        assert classify_license("MIT") == LicenseRisk.OK

    def test_apache(self):
        assert classify_license("Apache-2.0") == LicenseRisk.OK

    def test_bsd(self):
        assert classify_license("BSD-3-Clause") == LicenseRisk.OK

    def test_isc(self):
        assert classify_license("ISC") == LicenseRisk.OK

    def test_gpl(self):
        assert classify_license("GPL-3.0") == LicenseRisk.COPYLEFT

    def test_agpl(self):
        assert classify_license("AGPL-3.0") == LicenseRisk.COPYLEFT

    def test_lgpl(self):
        assert classify_license("LGPL-3.0") == LicenseRisk.WARNING

    def test_mpl(self):
        assert classify_license("MPL-2.0") == LicenseRisk.WARNING

    def test_empty(self):
        assert classify_license("") == LicenseRisk.UNKNOWN

    def test_unknown_license(self):
        assert classify_license("PROPRIETARY-XYZ") == LicenseRisk.UNKNOWN

    def test_case_insensitive(self):
        assert classify_license("mit") == LicenseRisk.OK
        assert classify_license("MIT") == LicenseRisk.OK

    def test_substring_mit(self):
        assert classify_license("MIT License") == LicenseRisk.OK

    def test_substring_apache(self):
        assert classify_license("Apache License 2.0") == LicenseRisk.OK

    def test_substring_gpl(self):
        assert classify_license("GNU General Public License v3 (gpl)") == LicenseRisk.COPYLEFT

    def test_unlicense(self):
        assert classify_license("Unlicense") == LicenseRisk.OK

    def test_cc0(self):
        assert classify_license("CC0-1.0") == LicenseRisk.OK

    def test_psf(self):
        assert classify_license("PSF") == LicenseRisk.OK


# --- Typosquatting Detection ---

class TestCheckTyposquat:
    def test_known_typosquat(self):
        assert _check_typosquat("reqests") == "requests"

    def test_legitimate_package(self):
        assert _check_typosquat("requests") is None

    def test_flask_typo(self):
        assert _check_typosquat("flaask") == "flask"

    def test_django_typo(self):
        assert _check_typosquat("djnago") == "django"

    def test_numpy_typo(self):
        assert _check_typosquat("numpi") == "numpy"

    def test_unknown_package(self):
        assert _check_typosquat("my-custom-lib") is None

    def test_case_insensitive(self):
        assert _check_typosquat("Reqests") is None or _check_typosquat("reqests") == "requests"


# --- Version Matching ---

class TestVersionMatches:
    def test_exact_match(self):
        assert _version_matches("1.0", ["1.0"]) is True

    def test_prefix_match(self):
        assert _version_matches("2.0.1", ["2.0"]) is True

    def test_no_match(self):
        assert _version_matches("3.0.0", ["2.0", "1.0"]) is False

    def test_empty_version(self):
        assert _version_matches("", ["1.0"]) is False

    def test_unpinned(self):
        assert _version_matches("unpinned", ["1.0"]) is False

    def test_caret_prefix(self):
        assert _version_matches("^1.0.0", ["1.0"]) is True

    def test_tilde_prefix(self):
        assert _version_matches("~2.0.1", ["2.0"]) is True

    def test_gte_prefix(self):
        assert _version_matches(">=3.0.0", ["3.0"]) is True

    def test_no_partial_match(self):
        # "2.0" should not match "2.01"
        assert _version_matches("2.01", ["2.0"]) is False


# --- Input Parsing ---

class TestParseInput:
    def test_requirements_format(self):
        text = "flask==2.0.0\nrequests>=2.28.0\n"
        deps = _parse_input(text)
        assert len(deps) == 2
        assert deps[0][0] == "flask"

    def test_package_json(self):
        text = json.dumps({"dependencies": {"express": "^4.18.0"}})
        deps = _parse_input(text)
        assert len(deps) == 1
        assert deps[0][0] == "express"

    def test_python_imports(self):
        text = "import flask\nfrom requests import get\n"
        deps = _parse_input(text)
        assert len(deps) >= 1
        names = [d[0] for d in deps]
        assert "flask" in names

    def test_stdlib_excluded(self):
        text = "import os\nimport sys\nimport json\nimport re\n"
        deps = _parse_input(text)
        assert len(deps) == 0

    def test_empty(self):
        deps = _parse_input("")
        assert deps == []


# --- scan_dependencies ---

class TestScanDependencies:
    def test_empty_input(self):
        report = scan_dependencies("")
        assert len(report.errors) > 0

    def test_safe_deps(self):
        report = scan_dependencies("flask==2.3.0\nclick==8.0.0\n")
        assert report.total_deps == 2
        assert report.risk_score >= 0

    def test_detects_risky(self):
        report = scan_dependencies("sh==1.14.0\n")
        risky = [f for f in report.findings if f.finding_type == "risky"]
        assert len(risky) >= 1

    def test_detects_deprecated(self):
        report = scan_dependencies("nose==1.3.7\n")
        deprecated = [f for f in report.findings if f.finding_type == "deprecated"]
        assert len(deprecated) >= 1

    def test_detects_typosquat(self):
        report = scan_dependencies("reqests==2.28.0\n")
        typos = [f for f in report.findings if f.finding_type == "typosquat"]
        assert len(typos) >= 1
        assert typos[0].severity == "CRITICAL"

    def test_detects_vulnerable_version(self):
        report = scan_dependencies("pyyaml==5.1\n")
        vulns = [f for f in report.findings if f.finding_type == "vulnerable"]
        assert len(vulns) >= 1

    def test_license_copyleft(self):
        report = scan_dependencies(
            "some-lib==1.0\n",
            licenses={"some-lib": "GPL-3.0"},
        )
        lic_issues = [f for f in report.findings if f.finding_type == "license"]
        assert len(lic_issues) >= 1
        assert lic_issues[0].severity == "HIGH"

    def test_license_warning(self):
        report = scan_dependencies(
            "some-lib==1.0\n",
            licenses={"some-lib": "LGPL-3.0"},
        )
        lic_issues = [f for f in report.findings if f.finding_type == "license"]
        assert len(lic_issues) >= 1
        assert lic_issues[0].severity == "MEDIUM"

    def test_license_ok_no_finding(self):
        report = scan_dependencies(
            "flask==2.3.0\n",
            licenses={"flask": "MIT"},
        )
        lic_issues = [f for f in report.findings if f.finding_type == "license"]
        assert len(lic_issues) == 0

    def test_risk_score_increases(self):
        r_safe = scan_dependencies("click==8.0.0\n")
        r_risky = scan_dependencies("sh==1.14.0\nparamiko==2.0.0\nreqests==1.0\n")
        assert r_risky.risk_score > r_safe.risk_score

    def test_multiple_finding_types(self):
        report = scan_dependencies("nose==1.3.7\nsh==1.14.0\nreqests==1.0\n")
        types = {f.finding_type for f in report.findings}
        assert "deprecated" in types
        assert "risky" in types
        assert "typosquat" in types

    def test_package_json_input(self):
        text = json.dumps({
            "dependencies": {
                "express": "^4.18.0",
                "shelljs": "^0.8.0",
            }
        })
        report = scan_dependencies(text)
        assert report.total_deps == 2
        risky = [f for f in report.findings if f.finding_type == "risky"]
        assert len(risky) >= 1

    def test_cve_in_finding(self):
        report = scan_dependencies("pyyaml==5.1\n")
        vulns = [f for f in report.findings if f.finding_type == "vulnerable"]
        if vulns:
            assert vulns[0].cve.startswith("CVE-")

    def test_report_to_dict(self):
        report = scan_dependencies("flask==2.3.0\n")
        d = report.to_dict()
        assert "total_deps" in d
        assert "findings" in d
        assert "risk_score" in d

    def test_node_serialize_vulnerable(self):
        text = json.dumps({"dependencies": {"node-serialize": "0.0.4"}})
        report = scan_dependencies(text)
        vulns = [f for f in report.findings if f.finding_type == "vulnerable"]
        assert len(vulns) >= 1

    def test_vm2_vulnerable(self):
        text = json.dumps({"dependencies": {"vm2": "3.9.10"}})
        report = scan_dependencies(text)
        vulns = [f for f in report.findings if f.finding_type == "vulnerable"]
        assert len(vulns) >= 1

    def test_pycrypto_deprecated(self):
        report = scan_dependencies("pycrypto==2.6.1\n")
        deprecated = [f for f in report.findings if f.finding_type == "deprecated"]
        assert len(deprecated) >= 1

    def test_mock_deprecated(self):
        report = scan_dependencies("mock==4.0.0\n")
        deprecated = [f for f in report.findings if f.finding_type == "deprecated"]
        assert len(deprecated) >= 1


# --- Formatters ---

class TestFormatDepReportText:
    def test_basic(self):
        report = DependencyReport(source="test", total_deps=5, risk_score=42)
        text = format_dep_report_text(report)
        assert "Dependency Analysis" in text
        assert "42/100" in text

    def test_with_findings(self):
        report = DependencyReport(
            source="test",
            total_deps=1,
            findings=[
                DepFinding("sh", "1.0", "risky", "HIGH", "Shell wrapper"),
            ],
        )
        text = format_dep_report_text(report)
        assert "RISKY" in text
        assert "sh" in text

    def test_with_typosquat(self):
        report = DependencyReport(
            source="test",
            findings=[
                DepFinding("reqests", "1.0", "typosquat", "CRITICAL", "Typo"),
            ],
        )
        text = format_dep_report_text(report)
        assert "TYPOSQUAT" in text

    def test_with_vulnerable(self):
        report = DependencyReport(
            source="test",
            findings=[
                DepFinding("flask", "0.12", "vulnerable", "HIGH", "Bug", cve="CVE-123"),
            ],
        )
        text = format_dep_report_text(report)
        assert "VULNERABILIT" in text
        assert "CVE-123" in text

    def test_no_findings(self):
        report = DependencyReport(source="test", total_deps=3)
        text = format_dep_report_text(report)
        assert "No issues found" in text

    def test_errors_shown(self):
        report = DependencyReport(errors=["Parse error"])
        text = format_dep_report_text(report)
        assert "Error:" in text


class TestFormatDepReportJson:
    def test_valid_json(self):
        report = DependencyReport(source="test", total_deps=2, risk_score=10)
        text = format_dep_report_json(report)
        data = json.loads(text)
        assert data["source"] == "test"
        assert data["total_deps"] == 2

    def test_findings_serialized(self):
        report = scan_dependencies("sh==1.14.0\n")
        text = format_dep_report_json(report)
        data = json.loads(text)
        assert isinstance(data["findings"], list)


# --- Public API ---

class TestExtendedPublicAPI:
    def test_get_deprecated_packages(self):
        deps = get_deprecated_packages()
        assert len(deps) >= 20
        names = {d.name for d in deps}
        assert "nose" in names
        assert "pycrypto" in names

    def test_get_vulnerable_versions(self):
        vulns = get_vulnerable_versions()
        assert len(vulns) >= 10
        packages = {v.package for v in vulns}
        assert "pyyaml" in packages
        assert "django" in packages

    def test_get_typosquat_targets(self):
        targets = get_typosquat_targets()
        assert "requests" in targets
        assert "flask" in targets
        assert len(targets) >= 15
