"""Tests for multi-target scanning — scan-all across directories."""

from __future__ import annotations

import json
import textwrap
from pathlib import Path

import pytest

from mcpaudit.multi import (
    FleetGrade,
    FleetReport,
    MultiScanner,
    ServerDetection,
    detect_mcp_servers,
    format_fleet_html,
    format_fleet_json,
    format_fleet_text,
)
from mcpaudit.models import Finding, ScanResult, ScanSummary, Severity


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def tmp_project(tmp_path: Path) -> Path:
    """Create a temporary project with multiple MCP server files."""
    # Python MCP server with a vulnerability (shell=True)
    py_server = tmp_path / "server_a" / "server.py"
    py_server.parent.mkdir(parents=True)
    py_server.write_text(textwrap.dedent("""\
        from mcp.server import Server
        import subprocess
        app = Server("demo")
        @app.tool()
        def run(cmd: str):
            return subprocess.call(cmd, shell=True)
    """), encoding="utf-8")

    # TypeScript MCP server with a vulnerability (dangerous function use)
    ts_server = tmp_path / "server_b" / "index.ts"
    ts_server.parent.mkdir(parents=True)
    ts_server.write_text(textwrap.dedent("""\
        import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
        const server = new McpServer({ name: "ts-demo" });
        server.tool("exec", async ({ cmd }) => {
            const result = Function("return " + cmd)();
            return { content: [{ type: "text", text: String(result) }] };
        });
    """), encoding="utf-8")

    # Non-MCP file (should be ignored)
    utils = tmp_path / "utils" / "helpers.py"
    utils.parent.mkdir(parents=True)
    utils.write_text("def add(a, b): return a + b\n", encoding="utf-8")

    # Nested MCP server
    nested = tmp_path / "deep" / "nested" / "mcp_server.py"
    nested.parent.mkdir(parents=True, exist_ok=True)
    nested.write_text(textwrap.dedent("""\
        from mcp.server import Server
        app = Server("nested")
        @app.tool()
        def safe_tool():
            return "hello"
    """), encoding="utf-8")

    return tmp_path


@pytest.fixture
def empty_dir(tmp_path: Path) -> Path:
    """Create an empty directory."""
    d = tmp_path / "empty"
    d.mkdir()
    return d


# ---------------------------------------------------------------------------
# Detection tests
# ---------------------------------------------------------------------------

class TestDetectMCPServers:
    """Tests for MCP server auto-detection."""

    def test_detect_python_server(self, tmp_project: Path) -> None:
        detections = detect_mcp_servers(tmp_project)
        py_paths = [d.path for d in detections if d.language == "python"]
        assert any("server_a" in str(p) for p in py_paths)

    def test_detect_typescript_server(self, tmp_project: Path) -> None:
        detections = detect_mcp_servers(tmp_project)
        ts_paths = [d.path for d in detections if d.language == "typescript"]
        assert any("server_b" in str(p) for p in ts_paths)

    def test_ignore_non_mcp_files(self, tmp_project: Path) -> None:
        detections = detect_mcp_servers(tmp_project)
        paths_str = [str(d.path) for d in detections]
        assert not any("helpers.py" in p for p in paths_str)

    def test_detect_nested_server(self, tmp_project: Path) -> None:
        detections = detect_mcp_servers(tmp_project)
        paths_str = [str(d.path) for d in detections]
        assert any("mcp_server.py" in p for p in paths_str)

    def test_empty_directory(self, empty_dir: Path) -> None:
        detections = detect_mcp_servers(empty_dir)
        assert detections == []

    def test_detection_has_pattern(self, tmp_project: Path) -> None:
        detections = detect_mcp_servers(tmp_project)
        for d in detections:
            assert d.pattern != ""

    def test_detection_dataclass_fields(self, tmp_project: Path) -> None:
        detections = detect_mcp_servers(tmp_project)
        for d in detections:
            assert isinstance(d.path, Path)
            assert d.language in ("python", "typescript")
            assert isinstance(d.pattern, str)

    def test_detect_create_server_pattern(self, tmp_path: Path) -> None:
        f = tmp_path / "app.ts"
        f.write_text('const s = createServer({ name: "x" });\n', encoding="utf-8")
        detections = detect_mcp_servers(tmp_path)
        assert len(detections) == 1
        assert detections[0].pattern == "createServer"

    def test_detect_fastmcp_pattern(self, tmp_path: Path) -> None:
        f = tmp_path / "app.py"
        f.write_text(textwrap.dedent("""\
            from fastmcp import FastMCP
            mcp = FastMCP("demo")
        """), encoding="utf-8")
        detections = detect_mcp_servers(tmp_path)
        assert len(detections) == 1
        assert detections[0].pattern == "FastMCP"

    def test_detect_stdio_transport(self, tmp_path: Path) -> None:
        f = tmp_path / "srv.py"
        f.write_text(textwrap.dedent("""\
            from mcp.server.stdio import stdio_server
            async def main():
                async with stdio_server() as (r, w):
                    pass
        """), encoding="utf-8")
        detections = detect_mcp_servers(tmp_path)
        assert len(detections) == 1

    def test_nonexistent_directory(self) -> None:
        detections = detect_mcp_servers(Path("/nonexistent/path"))
        assert detections == []

    def test_respects_exclude_dirs(self, tmp_path: Path) -> None:
        # Create server inside node_modules (should be excluded)
        nm = tmp_path / "node_modules" / "pkg" / "server.py"
        nm.parent.mkdir(parents=True)
        nm.write_text("from mcp.server import Server\n", encoding="utf-8")
        detections = detect_mcp_servers(tmp_path)
        assert len(detections) == 0


# ---------------------------------------------------------------------------
# MultiScanner tests
# ---------------------------------------------------------------------------

class TestMultiScanner:
    """Tests for multi-target scanning engine."""

    def test_scan_all_returns_fleet_report(self, tmp_project: Path) -> None:
        ms = MultiScanner()
        report = ms.scan_all(tmp_project)
        assert isinstance(report, FleetReport)

    def test_fleet_report_has_servers(self, tmp_project: Path) -> None:
        ms = MultiScanner()
        report = ms.scan_all(tmp_project)
        assert report.total_servers >= 2  # at least py + ts

    def test_fleet_report_total_findings(self, tmp_project: Path) -> None:
        ms = MultiScanner()
        report = ms.scan_all(tmp_project)
        assert report.total_findings >= 0

    def test_fleet_report_severity_counts(self, tmp_project: Path) -> None:
        ms = MultiScanner()
        report = ms.scan_all(tmp_project)
        counts = report.severity_counts
        assert "CRITICAL" in counts
        assert "HIGH" in counts

    def test_fleet_grade_computed(self, tmp_project: Path) -> None:
        ms = MultiScanner()
        report = ms.scan_all(tmp_project)
        assert report.fleet_grade.value in ("A+", "A", "B", "C", "D", "F", "N/A")

    def test_fleet_report_to_dict(self, tmp_project: Path) -> None:
        ms = MultiScanner()
        report = ms.scan_all(tmp_project)
        d = report.to_dict()
        assert "total_servers" in d
        assert "fleet_grade" in d
        assert "severity_counts" in d
        assert "servers" in d

    def test_empty_directory_fleet(self, empty_dir: Path) -> None:
        ms = MultiScanner()
        report = ms.scan_all(empty_dir)
        assert report.total_servers == 0
        assert report.fleet_grade == FleetGrade.NO_SERVERS

    def test_scan_time_positive(self, tmp_project: Path) -> None:
        ms = MultiScanner()
        report = ms.scan_all(tmp_project)
        assert report.scan_time_ms >= 0


# ---------------------------------------------------------------------------
# FleetGrade tests
# ---------------------------------------------------------------------------

class TestFleetGrade:
    """Tests for fleet grade calculation."""

    def test_all_clean_is_a_plus(self) -> None:
        report = FleetReport(
            servers=[],
            detections=[],
            scan_time_ms=0.0,
        )
        report.servers = [
            _make_server_result("a.py", grade="A+"),
            _make_server_result("b.py", grade="A+"),
        ]
        assert report.fleet_grade == FleetGrade.A_PLUS

    def test_worst_grade_wins(self) -> None:
        report = FleetReport(
            servers=[],
            detections=[],
            scan_time_ms=0.0,
        )
        report.servers = [
            _make_server_result("a.py", grade="A+"),
            _make_server_result("b.py", grade="D"),
        ]
        assert report.fleet_grade == FleetGrade.D

    def test_single_f_makes_fleet_f(self) -> None:
        report = FleetReport(
            servers=[],
            detections=[],
            scan_time_ms=0.0,
        )
        report.servers = [
            _make_server_result("a.py", grade="A"),
            _make_server_result("b.py", grade="F"),
        ]
        assert report.fleet_grade == FleetGrade.F

    def test_no_servers_grade(self) -> None:
        report = FleetReport(servers=[], detections=[], scan_time_ms=0.0)
        assert report.fleet_grade == FleetGrade.NO_SERVERS


# ---------------------------------------------------------------------------
# Formatting tests
# ---------------------------------------------------------------------------

class TestFleetFormatting:
    """Tests for fleet report formatting."""

    def test_format_text_contains_header(self, tmp_project: Path) -> None:
        ms = MultiScanner()
        report = ms.scan_all(tmp_project)
        text = format_fleet_text(report)
        assert "Fleet Security Report" in text

    def test_format_text_contains_grade(self, tmp_project: Path) -> None:
        ms = MultiScanner()
        report = ms.scan_all(tmp_project)
        text = format_fleet_text(report)
        assert "Fleet Grade" in text

    def test_format_json_valid(self, tmp_project: Path) -> None:
        ms = MultiScanner()
        report = ms.scan_all(tmp_project)
        json_str = format_fleet_json(report)
        data = json.loads(json_str)
        assert "total_servers" in data
        assert "fleet_grade" in data

    def test_format_json_servers_list(self, tmp_project: Path) -> None:
        ms = MultiScanner()
        report = ms.scan_all(tmp_project)
        data = json.loads(format_fleet_json(report))
        assert isinstance(data["servers"], list)

    def test_format_html_valid(self, tmp_project: Path) -> None:
        ms = MultiScanner()
        report = ms.scan_all(tmp_project)
        html = format_fleet_html(report)
        assert "<!DOCTYPE html>" in html
        assert "Fleet" in html

    def test_format_html_has_table(self, tmp_project: Path) -> None:
        ms = MultiScanner()
        report = ms.scan_all(tmp_project)
        html = format_fleet_html(report)
        assert "<table" in html

    def test_format_text_empty_fleet(self, empty_dir: Path) -> None:
        ms = MultiScanner()
        report = ms.scan_all(empty_dir)
        text = format_fleet_text(report)
        assert "No MCP servers" in text

    def test_format_json_empty_fleet(self, empty_dir: Path) -> None:
        ms = MultiScanner()
        report = ms.scan_all(empty_dir)
        data = json.loads(format_fleet_json(report))
        assert data["total_servers"] == 0

    def test_format_text_shows_server_paths(self, tmp_project: Path) -> None:
        ms = MultiScanner()
        report = ms.scan_all(tmp_project)
        text = format_fleet_text(report)
        # At least one server path should appear
        assert "server" in text.lower()


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_server_result(
    filepath: str,
    grade: str,
) -> dict:
    """Create a minimal server result dict for testing."""
    grade_scores = {"A+": 0, "A": 1, "B": 4, "C": 8, "D": 15, "F": 25}
    score = grade_scores.get(grade, 0)
    findings = []
    if score > 0:
        findings = [
            Finding(
                rule_id="TEST-001",
                severity=Severity.MEDIUM,
                title="Test finding",
                description="For testing",
                file=filepath,
                line=1,
            )
        ] * (score // 2)
    return {
        "file": filepath,
        "grade": grade,
        "findings_count": len(findings),
        "scan_result": ScanResult(
            file=filepath,
            findings=findings,
        ),
    }
