"""Tests for Dependency Scanner."""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from mcpaudit.dep_scanner import (
    DependencyScanner,
    DepScanResult,
    VulnerableDep,
    _detect_manifest_type,
    _parse_cargo_toml,
    _parse_go_mod,
    _parse_package_json,
    _parse_requirements_txt,
    _parse_version,
    _version_lt,
    format_dep_scan_json,
    format_dep_scan_text,
)


# --- Version parsing ---

class TestVersionParsing:
    def test_simple_version(self):
        assert _parse_version("1.2.3") == (1, 2, 3)

    def test_two_part_version(self):
        assert _parse_version("1.2") == (1, 2, 0)

    def test_one_part_version(self):
        assert _parse_version("1") == (1, 0, 0)

    def test_with_operator(self):
        assert _parse_version(">=1.2.3") == (1, 2, 3)

    def test_with_caret(self):
        assert _parse_version("^1.2.3") == (1, 2, 3)

    def test_with_tilde(self):
        assert _parse_version("~1.2.3") == (1, 2, 3)

    def test_with_prerelease(self):
        assert _parse_version("1.2.3-beta.1") == (1, 2, 3)

    def test_empty_string(self):
        assert _parse_version("") == (0, 0, 0)

    def test_invalid_string(self):
        assert _parse_version("abc") == (0, 0, 0)

    def test_version_lt_true(self):
        assert _version_lt("1.0.0", "2.0.0") is True

    def test_version_lt_false(self):
        assert _version_lt("2.0.0", "1.0.0") is False

    def test_version_lt_equal(self):
        assert _version_lt("1.0.0", "1.0.0") is False

    def test_version_lt_minor(self):
        assert _version_lt("1.1.0", "1.2.0") is True


# --- Manifest type detection ---

class TestManifestDetection:
    def test_requirements_txt(self):
        assert _detect_manifest_type(Path("requirements.txt")) == "python"

    def test_package_json(self):
        assert _detect_manifest_type(Path("package.json")) == "node"

    def test_go_mod(self):
        assert _detect_manifest_type(Path("go.mod")) == "go"

    def test_cargo_toml(self):
        assert _detect_manifest_type(Path("Cargo.toml")) == "rust"

    def test_unknown(self):
        assert _detect_manifest_type(Path("README.md")) == "unknown"

    def test_requirements_dev(self):
        assert _detect_manifest_type(Path("requirements-dev.txt")) == "python"


# --- requirements.txt parsing ---

class TestRequirementsTxtParsing:
    def test_pinned_version(self):
        content = "flask==1.1.4\ndjango==3.1.0\n"
        deps = _parse_requirements_txt(content)
        assert ("flask", "1.1.4") in deps
        assert ("django", "3.1.0") in deps

    def test_gte_version(self):
        content = "requests>=2.25.0\n"
        deps = _parse_requirements_txt(content)
        assert ("requests", "2.25.0") in deps

    def test_no_version(self):
        content = "flask\ndjango\n"
        deps = _parse_requirements_txt(content)
        assert ("flask", "") in deps

    def test_comments_ignored(self):
        content = "# This is a comment\nflask==1.0\n"
        deps = _parse_requirements_txt(content)
        assert len(deps) == 1

    def test_inline_comment(self):
        content = "flask==1.0  # web framework\n"
        deps = _parse_requirements_txt(content)
        assert ("flask", "1.0") in deps

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

    def test_git_url_skipped(self):
        content = "git+https://github.com/user/repo.git\nflask==1.0\n"
        deps = _parse_requirements_txt(content)
        assert len(deps) == 1


# --- package.json parsing ---

class TestPackageJsonParsing:
    def test_dependencies(self):
        content = json.dumps({
            "dependencies": {
                "express": "^4.17.0",
                "axios": "~1.2.0",
            }
        })
        deps = _parse_package_json(content)
        assert ("express", "^4.17.0") in deps
        assert ("axios", "~1.2.0") in deps

    def test_dev_dependencies(self):
        content = json.dumps({
            "devDependencies": {
                "jest": "^29.0.0",
            }
        })
        deps = _parse_package_json(content)
        assert ("jest", "^29.0.0") in deps

    def test_both_sections(self):
        content = json.dumps({
            "dependencies": {"express": "4.17.0"},
            "devDependencies": {"jest": "29.0.0"},
        })
        deps = _parse_package_json(content)
        assert len(deps) == 2

    def test_invalid_json(self):
        deps = _parse_package_json("not json")
        assert deps == []

    def test_empty_deps(self):
        content = json.dumps({"name": "test"})
        deps = _parse_package_json(content)
        assert deps == []


# --- go.mod parsing ---

class TestGoModParsing:
    def test_require_block(self):
        content = """module example.com/myapp

go 1.21

require (
    github.com/gin-gonic/gin v1.9.0
    github.com/lib/pq v1.10.0
)
"""
        deps = _parse_go_mod(content)
        assert ("github.com/gin-gonic/gin", "1.9.0") in deps
        assert ("github.com/lib/pq", "1.10.0") in deps

    def test_single_require(self):
        content = """module example.com/myapp

require github.com/gorilla/mux v1.8.0
"""
        deps = _parse_go_mod(content)
        assert ("github.com/gorilla/mux", "1.8.0") in deps

    def test_comments_ignored(self):
        content = """module example.com/myapp

require (
    // indirect
    github.com/foo/bar v1.0.0
)
"""
        deps = _parse_go_mod(content)
        assert len(deps) == 1

    def test_empty(self):
        deps = _parse_go_mod("module example.com/myapp\n")
        assert deps == []


# --- Cargo.toml parsing ---

class TestCargoTomlParsing:
    def test_simple_deps(self):
        content = """[package]
name = "myapp"

[dependencies]
serde = "1.0"
tokio = "1.28"
"""
        deps = _parse_cargo_toml(content)
        assert ("serde", "1.0") in deps
        assert ("tokio", "1.28") in deps

    def test_table_format(self):
        content = """[dependencies]
tokio = { version = "1.28", features = ["full"] }
"""
        deps = _parse_cargo_toml(content)
        assert ("tokio", "1.28") in deps

    def test_dev_dependencies(self):
        content = """[dev-dependencies]
criterion = "0.5"
"""
        deps = _parse_cargo_toml(content)
        assert ("criterion", "0.5") in deps

    def test_empty(self):
        deps = _parse_cargo_toml("[package]\nname = 'test'\n")
        assert deps == []


# --- DependencyScanner ---

class TestDependencyScanner:
    def test_scan_requirements_vulnerable(self, tmp_path):
        req_file = tmp_path / "requirements.txt"
        req_file.write_text("flask==1.1.4\ndjango==3.0.0\nrequests==2.25.0\n")

        scanner = DependencyScanner()
        result = scanner.scan_manifest(req_file)
        assert result.ecosystem == "python"
        assert result.total_deps == 3
        assert result.vuln_count >= 3  # All three are vulnerable

    def test_scan_requirements_safe(self, tmp_path):
        req_file = tmp_path / "requirements.txt"
        req_file.write_text("flask==3.0.0\ndjango==4.2.0\nrequests==2.31.0\n")

        scanner = DependencyScanner()
        result = scanner.scan_manifest(req_file)
        assert result.vuln_count == 0

    def test_scan_package_json_vulnerable(self, tmp_path):
        pkg_file = tmp_path / "package.json"
        pkg_file.write_text(json.dumps({
            "dependencies": {
                "express": "4.17.0",
                "lodash": "4.17.20",
            }
        }))

        scanner = DependencyScanner()
        result = scanner.scan_manifest(pkg_file)
        assert result.ecosystem == "node"
        assert result.vuln_count >= 2

    def test_scan_package_json_safe(self, tmp_path):
        pkg_file = tmp_path / "package.json"
        pkg_file.write_text(json.dumps({
            "dependencies": {
                "express": "4.19.0",
                "lodash": "4.17.21",
            }
        }))

        scanner = DependencyScanner()
        result = scanner.scan_manifest(pkg_file)
        assert result.vuln_count == 0

    def test_scan_unpinned_version(self, tmp_path):
        req_file = tmp_path / "requirements.txt"
        req_file.write_text("flask\ndjango\n")

        scanner = DependencyScanner()
        result = scanner.scan_manifest(req_file)
        unpinned = [v for v in result.vulnerable if v.current_version == "unpinned"]
        assert len(unpinned) >= 2

    def test_scan_unknown_manifest(self, tmp_path):
        txt_file = tmp_path / "README.md"
        txt_file.write_text("# Hello\n")

        scanner = DependencyScanner()
        result = scanner.scan_manifest(txt_file)
        assert result.ecosystem == "unknown"
        assert "Unknown" in result.scan_error

    def test_scan_directory(self, tmp_path):
        req_file = tmp_path / "requirements.txt"
        req_file.write_text("flask==1.0.0\n")
        pkg_file = tmp_path / "package.json"
        pkg_file.write_text(json.dumps({"dependencies": {"express": "4.17.0"}}))

        scanner = DependencyScanner()
        results = scanner.scan_directory(tmp_path)
        assert len(results) >= 2

    def test_scan_directory_skips_node_modules(self, tmp_path):
        nm_dir = tmp_path / "node_modules" / "express"
        nm_dir.mkdir(parents=True)
        (nm_dir / "package.json").write_text(json.dumps({"dependencies": {}}))
        (tmp_path / "package.json").write_text(json.dumps({"dependencies": {}}))

        scanner = DependencyScanner()
        results = scanner.scan_directory(tmp_path)
        assert len(results) == 1

    def test_extra_vulns(self, tmp_path):
        req_file = tmp_path / "requirements.txt"
        req_file.write_text("mypackage==1.0.0\n")

        scanner = DependencyScanner(extra_vulns={
            "python": [("mypackage", "2.0.0", "2.0.0", "HIGH", "Custom vuln")]
        })
        result = scanner.scan_manifest(req_file)
        assert result.vuln_count >= 1


# --- VulnerableDep ---

class TestVulnerableDep:
    def test_to_dict(self):
        dep = VulnerableDep(
            package="flask",
            current_version="1.0.0",
            safe_version="2.0.0",
            severity="HIGH",
            advisory="Test advisory",
            manifest_file="requirements.txt",
            ecosystem="python",
        )
        d = dep.to_dict()
        assert d["package"] == "flask"
        assert d["severity"] == "HIGH"


# --- DepScanResult ---

class TestDepScanResult:
    def test_severity_counts(self):
        result = DepScanResult(
            manifest_file="test",
            ecosystem="python",
            vulnerable=[
                VulnerableDep("a", "1.0", "2.0", "CRITICAL", "x"),
                VulnerableDep("b", "1.0", "2.0", "HIGH", "x"),
                VulnerableDep("c", "1.0", "2.0", "HIGH", "x"),
            ],
        )
        counts = result.severity_counts
        assert counts["CRITICAL"] == 1
        assert counts["HIGH"] == 2

    def test_exit_code_critical(self):
        result = DepScanResult(
            manifest_file="test",
            ecosystem="python",
            vulnerable=[
                VulnerableDep("a", "1.0", "2.0", "CRITICAL", "x"),
            ],
        )
        assert result.exit_code == 2

    def test_exit_code_high(self):
        result = DepScanResult(
            manifest_file="test",
            ecosystem="python",
            vulnerable=[
                VulnerableDep("a", "1.0", "2.0", "HIGH", "x"),
            ],
        )
        assert result.exit_code == 1

    def test_exit_code_clean(self):
        result = DepScanResult(
            manifest_file="test",
            ecosystem="python",
        )
        assert result.exit_code == 0


# --- Formatting ---

class TestFormatting:
    def test_format_text(self):
        result = DepScanResult(
            manifest_file="requirements.txt",
            ecosystem="python",
            total_deps=3,
            vulnerable=[
                VulnerableDep("flask", "1.0.0", "2.0.0", "HIGH", "Test advisory"),
            ],
        )
        text = format_dep_scan_text(result)
        assert "flask" in text
        assert "requirements.txt" in text
        assert "HIGH" in text

    def test_format_text_clean(self):
        result = DepScanResult(
            manifest_file="requirements.txt",
            ecosystem="python",
            total_deps=3,
        )
        text = format_dep_scan_text(result)
        assert "No known vulnerabilities" in text

    def test_format_json(self):
        result = DepScanResult(
            manifest_file="requirements.txt",
            ecosystem="python",
            total_deps=1,
            vulnerable=[
                VulnerableDep("flask", "1.0.0", "2.0.0", "HIGH", "Test"),
            ],
        )
        output = format_dep_scan_json(result)
        data = json.loads(output)
        assert data["ecosystem"] == "python"
        assert data["vulnerable_count"] == 1
