"""Tests for the MCP Server Profiler (mcpaudit.server_profiler).

Tests tool/resource/prompt detection, framework detection, transport,
fingerprinting, complexity scoring, profile comparison, and format helpers.
"""

from __future__ import annotations

import json

import pytest

from mcpaudit.server_profiler import (
    FrameworkDetection,
    ProfileComparison,
    ProfileDiff,
    PromptInfo,
    ResourceInfo,
    ServerProfile,
    ToolInfo,
    TransportInfo,
    compare_profiles,
    format_comparison_json,
    format_comparison_text,
    format_profile_json,
    format_profile_text,
    profile_server,
    _compute_complexity,
    _compute_fingerprint,
    _detect_dependencies,
    _detect_framework,
    _detect_resources,
    _detect_server_name,
    _detect_tools,
    _detect_transport,
    _detect_version,
    _detect_prompts,
    _extract_params,
)


# ---------------------------------------------------------------------------
# Sample code snippets
# ---------------------------------------------------------------------------

FASTMCP_SERVER = '''
from fastmcp import FastMCP

server = FastMCP("my-test-server")

@server.tool()
async def read_file(path: str) -> str:
    """Read a file."""
    with open(path) as f:
        return f.read()

@server.tool()
async def write_file(path: str, content: str) -> bool:
    """Write a file with auth check."""
    if not check_auth(token):
        raise PermissionError("Not authorized")
    with open(path, "w") as f:
        f.write(content)
    return True

@server.resource("docs://readme")
async def get_readme() -> str:
    return "README content"

@server.resource("config://settings")
async def get_settings() -> str:
    return "{}"

@server.prompt()
async def summarize(text: str) -> str:
    return f"Summarize: {text}"

if __name__ == "__main__":
    server.run_stdio()
'''

MCP_PYTHON_SDK_SERVER = '''
from mcp.server import Server
from mcp.server.stdio import stdio_server
import anthropic

server = Server("sdk-server")

@server.tool()
def search(query: str, limit: int = 10) -> str:
    return "results"

@server.resource("data://users")
def get_users() -> str:
    return "users"
'''

TS_SERVER = '''
import { Server } from "@modelcontextprotocol/sdk";
const server = new Server("ts-server");
server.tool("greet", async (name) => `Hello ${name}`);
'''

MINIMAL_SERVER = '''
# A minimal python script
print("hello world")
'''

SSE_SERVER = '''
from mcp.server import Server
from mcp.server.sse import SseServerTransport

server = Server("sse-server")
transport = SseServerTransport()
'''

HTTP_TLS_SERVER = '''
from mcp.server import Server

server = Server("secure-server")
# Using HTTPS transport with TLS
server.run(transport="StreamableHTTPTransport", port=8443, tls=True)
'''

GO_SERVER = '''
package main

import (
    "github.com/mark3labs/mcp-go/mcp"
)

func main() {
    server := mcp.NewServer("go-server")
}
'''


# ===========================================================================
# ToolInfo
# ===========================================================================

class TestToolInfo:
    def test_to_dict(self) -> None:
        t = ToolInfo(name="read_file", parameters=["path"],
                     has_auth=True, has_validation=False, line=10)
        d = t.to_dict()
        assert d["name"] == "read_file"
        assert d["has_auth"] is True
        assert d["parameters"] == ["path"]

    def test_defaults(self) -> None:
        t = ToolInfo(name="test")
        assert t.description == ""
        assert t.parameters == []
        assert t.has_auth is False


# ===========================================================================
# ResourceInfo
# ===========================================================================

class TestResourceInfo:
    def test_to_dict(self) -> None:
        r = ResourceInfo(uri="docs://readme", name="readme", line=5)
        d = r.to_dict()
        assert d["uri"] == "docs://readme"

    def test_defaults(self) -> None:
        r = ResourceInfo(uri="test://x")
        assert r.name == ""
        assert r.mime_type == ""


# ===========================================================================
# PromptInfo
# ===========================================================================

class TestPromptInfo:
    def test_to_dict(self) -> None:
        p = PromptInfo(name="summarize", arguments=["text"], line=20)
        d = p.to_dict()
        assert d["name"] == "summarize"
        assert d["arguments"] == ["text"]


# ===========================================================================
# TransportInfo
# ===========================================================================

class TestTransportInfo:
    def test_to_dict(self) -> None:
        t = TransportInfo(type="sse", secure=True, port=8080)
        d = t.to_dict()
        assert d["type"] == "sse"
        assert d["secure"] is True

    def test_defaults(self) -> None:
        t = TransportInfo(type="stdio")
        assert t.secure is False
        assert t.port == 0


# ===========================================================================
# FrameworkDetection
# ===========================================================================

class TestFrameworkDetection:
    def test_to_dict(self) -> None:
        f = FrameworkDetection(name="FastMCP", version_hint="0.3.0",
                               confidence="HIGH", language="python")
        d = f.to_dict()
        assert d["name"] == "FastMCP"


# ===========================================================================
# ProfileDiff
# ===========================================================================

class TestProfileDiff:
    def test_to_dict(self) -> None:
        d = ProfileDiff(aspect="tools_count", value_a=3, value_b=5, status="changed")
        result = d.to_dict()
        assert result["status"] == "changed"

    def test_defaults(self) -> None:
        d = ProfileDiff(aspect="test")
        assert d.status == "same"


# ===========================================================================
# ServerProfile
# ===========================================================================

class TestServerProfile:
    def test_to_dict(self) -> None:
        p = ServerProfile(name="test", fingerprint="abc123",
                          complexity_score=10, complexity_label="Simple")
        d = p.to_dict()
        assert d["name"] == "test"
        assert d["fingerprint"] == "abc123"

    def test_defaults(self) -> None:
        p = ServerProfile()
        assert p.tools == []
        assert p.resources == []
        assert p.lines_of_code == 0

    def test_with_tools_and_resources(self) -> None:
        p = ServerProfile(
            tools=[ToolInfo(name="t1"), ToolInfo(name="t2")],
            resources=[ResourceInfo(uri="r://1")],
        )
        d = p.to_dict()
        assert len(d["tools"]) == 2
        assert len(d["resources"]) == 1


# ===========================================================================
# ProfileComparison
# ===========================================================================

class TestProfileComparison:
    def test_to_dict(self) -> None:
        c = ProfileComparison(
            profile_a_name="A", profile_b_name="B",
            tools_added=["new_tool"], complexity_delta=5,
        )
        d = c.to_dict()
        assert d["tools_added"] == ["new_tool"]
        assert d["complexity_delta"] == 5

    def test_defaults(self) -> None:
        c = ProfileComparison()
        assert c.fingerprint_match is False
        assert c.diffs == []


# ===========================================================================
# _detect_tools
# ===========================================================================

class TestDetectTools:
    def test_fastmcp_tools(self) -> None:
        tools = _detect_tools(FASTMCP_SERVER)
        names = {t.name for t in tools}
        assert "read_file" in names
        assert "write_file" in names

    def test_mcp_sdk_tools(self) -> None:
        tools = _detect_tools(MCP_PYTHON_SDK_SERVER)
        names = {t.name for t in tools}
        assert "search" in names

    def test_ts_tools(self) -> None:
        tools = _detect_tools(TS_SERVER)
        names = {t.name for t in tools}
        assert "greet" in names

    def test_no_tools(self) -> None:
        assert _detect_tools(MINIMAL_SERVER) == []

    def test_auth_detection(self) -> None:
        tools = _detect_tools(FASTMCP_SERVER)
        write_tool = next(t for t in tools if t.name == "write_file")
        assert write_tool.has_auth is True

    def test_line_numbers(self) -> None:
        tools = _detect_tools(FASTMCP_SERVER)
        for t in tools:
            assert t.line > 0

    def test_no_duplicates(self) -> None:
        code = FASTMCP_SERVER + "\n" + FASTMCP_SERVER
        tools = _detect_tools(code)
        names = [t.name for t in tools]
        assert len(names) == len(set(names))


# ===========================================================================
# _detect_resources
# ===========================================================================

class TestDetectResources:
    def test_fastmcp_resources(self) -> None:
        resources = _detect_resources(FASTMCP_SERVER)
        uris = {r.uri for r in resources}
        assert "docs://readme" in uris
        assert "config://settings" in uris

    def test_mcp_sdk_resources(self) -> None:
        resources = _detect_resources(MCP_PYTHON_SDK_SERVER)
        uris = {r.uri for r in resources}
        assert "data://users" in uris

    def test_no_resources(self) -> None:
        assert _detect_resources(MINIMAL_SERVER) == []


# ===========================================================================
# _detect_prompts
# ===========================================================================

class TestDetectPrompts:
    def test_fastmcp_prompts(self) -> None:
        prompts = _detect_prompts(FASTMCP_SERVER)
        names = {p.name for p in prompts}
        assert "summarize" in names

    def test_no_prompts(self) -> None:
        assert _detect_prompts(MINIMAL_SERVER) == []


# ===========================================================================
# _detect_framework
# ===========================================================================

class TestDetectFramework:
    def test_fastmcp(self) -> None:
        fw = _detect_framework(FASTMCP_SERVER)
        assert fw.name == "FastMCP"
        assert fw.language == "python"
        assert fw.confidence == "HIGH"

    def test_mcp_python_sdk(self) -> None:
        fw = _detect_framework(MCP_PYTHON_SDK_SERVER)
        assert fw.name in ("FastMCP", "mcp-python-sdk")

    def test_ts_sdk(self) -> None:
        fw = _detect_framework(TS_SERVER)
        assert fw.name == "@modelcontextprotocol/sdk"
        assert fw.language == "typescript"

    def test_go_mcp(self) -> None:
        fw = _detect_framework(GO_SERVER)
        assert fw.name == "mcp-go"
        assert fw.language == "go"

    def test_unknown(self) -> None:
        fw = _detect_framework(MINIMAL_SERVER)
        assert fw.confidence == "LOW"

    def test_custom_server(self) -> None:
        code = "class MyMCPServer:\n    pass"
        fw = _detect_framework(code)
        assert fw.name == "custom"


# ===========================================================================
# _detect_transport
# ===========================================================================

class TestDetectTransport:
    def test_stdio(self) -> None:
        t = _detect_transport(FASTMCP_SERVER)
        assert t.type == "stdio"

    def test_sse(self) -> None:
        t = _detect_transport(SSE_SERVER)
        assert t.type == "sse"

    def test_http_tls(self) -> None:
        t = _detect_transport(HTTP_TLS_SERVER)
        assert t.type == "http"
        assert t.secure is True
        assert t.port == 8443

    def test_default_stdio(self) -> None:
        t = _detect_transport(MINIMAL_SERVER)
        assert t.type == "stdio"


# ===========================================================================
# _detect_version
# ===========================================================================

class TestDetectVersion:
    def test_dunder_version(self) -> None:
        assert _detect_version('__version__ = "1.2.3"') == "1.2.3"

    def test_json_version(self) -> None:
        assert _detect_version('"version": "2.0.0"') == "2.0.0"

    def test_no_version(self) -> None:
        assert _detect_version("print('hello')") == ""


# ===========================================================================
# _detect_dependencies
# ===========================================================================

class TestDetectDependencies:
    def test_python_imports(self) -> None:
        deps = _detect_dependencies(MCP_PYTHON_SDK_SERVER)
        assert "mcp" in deps or "anthropic" in deps

    def test_excludes_stdlib(self) -> None:
        code = "import os\nimport json\nimport sys"
        deps = _detect_dependencies(code)
        assert "os" not in deps
        assert "json" not in deps

    def test_empty(self) -> None:
        assert _detect_dependencies("print('hello')") == []


# ===========================================================================
# _detect_server_name
# ===========================================================================

class TestDetectServerName:
    def test_fastmcp(self) -> None:
        assert _detect_server_name(FASTMCP_SERVER) == "my-test-server"

    def test_mcp_sdk(self) -> None:
        assert _detect_server_name(MCP_PYTHON_SDK_SERVER) == "sdk-server"

    def test_no_name(self) -> None:
        assert _detect_server_name(MINIMAL_SERVER) == ""


# ===========================================================================
# _extract_params
# ===========================================================================

class TestExtractParams:
    def test_basic(self) -> None:
        code = "(path: str, content: str) -> bool:"
        params = _extract_params(code, 0)
        assert "path" in params
        assert "content" in params

    def test_with_self(self) -> None:
        code = "(self, name: str):"
        params = _extract_params(code, 0)
        assert "self" not in params
        assert "name" in params

    def test_no_params(self) -> None:
        code = "() -> None:"
        params = _extract_params(code, 0)
        assert params == []


# ===========================================================================
# _compute_fingerprint
# ===========================================================================

class TestComputeFingerprint:
    def test_deterministic(self) -> None:
        p = ServerProfile(name="test", tools=[ToolInfo(name="t1")])
        fp1 = _compute_fingerprint(p)
        fp2 = _compute_fingerprint(p)
        assert fp1 == fp2

    def test_different_profiles_different_fingerprint(self) -> None:
        p1 = ServerProfile(name="test", tools=[ToolInfo(name="t1")])
        p2 = ServerProfile(name="test", tools=[ToolInfo(name="t2")])
        assert _compute_fingerprint(p1) != _compute_fingerprint(p2)

    def test_length(self) -> None:
        p = ServerProfile(name="test")
        fp = _compute_fingerprint(p)
        assert len(fp) == 16


# ===========================================================================
# _compute_complexity
# ===========================================================================

class TestComputeComplexity:
    def test_minimal(self) -> None:
        p = ServerProfile()
        score, label = _compute_complexity(p)
        assert score == 0
        assert label == "Minimal"

    def test_simple(self) -> None:
        p = ServerProfile(tools=[ToolInfo(name="t1"), ToolInfo(name="t2")])
        score, label = _compute_complexity(p)
        assert score == 6
        assert label == "Simple"

    def test_moderate(self) -> None:
        p = ServerProfile(
            tools=[ToolInfo(name=f"t{i}") for i in range(4)],
            resources=[ResourceInfo(uri=f"r://{i}") for i in range(2)],
        )
        score, label = _compute_complexity(p)
        assert score == 16
        assert label == "Moderate"

    def test_complex(self) -> None:
        p = ServerProfile(
            tools=[ToolInfo(name=f"t{i}") for i in range(10)],
            dependencies=["dep1", "dep2", "dep3"],
        )
        score, label = _compute_complexity(p)
        assert score >= 30
        assert label in ("Complex", "Very Complex")


# ===========================================================================
# profile_server
# ===========================================================================

class TestProfileServer:
    def test_fastmcp_profile(self) -> None:
        p = profile_server(FASTMCP_SERVER, file_path="server.py")
        assert p.name == "my-test-server"
        assert len(p.tools) >= 2
        assert len(p.resources) >= 2
        assert p.framework is not None
        assert p.framework.name == "FastMCP"
        assert p.transport is not None
        assert p.transport.type == "stdio"
        assert p.fingerprint
        assert p.complexity_score > 0
        assert "tools" in p.capabilities
        assert "resources" in p.capabilities

    def test_sdk_profile(self) -> None:
        p = profile_server(MCP_PYTHON_SDK_SERVER)
        assert len(p.tools) >= 1
        assert len(p.resources) >= 1

    def test_ts_profile(self) -> None:
        p = profile_server(TS_SERVER)
        assert len(p.tools) >= 1

    def test_minimal_profile(self) -> None:
        p = profile_server(MINIMAL_SERVER)
        assert len(p.tools) == 0
        assert p.complexity_label == "Minimal"

    def test_file_path_stored(self) -> None:
        p = profile_server(FASTMCP_SERVER, file_path="/tmp/server.py")
        assert p.file_path == "/tmp/server.py"

    def test_lines_of_code(self) -> None:
        p = profile_server(FASTMCP_SERVER)
        assert p.lines_of_code > 10

    def test_timestamp_set(self) -> None:
        p = profile_server(FASTMCP_SERVER)
        assert p.timestamp

    def test_capabilities_prompts(self) -> None:
        p = profile_server(FASTMCP_SERVER)
        assert "prompts" in p.capabilities

    def test_name_from_filepath_if_not_detected(self) -> None:
        p = profile_server(MINIMAL_SERVER, file_path="custom_server.py")
        assert "custom_server.py" in p.name


# ===========================================================================
# compare_profiles
# ===========================================================================

class TestCompareProfiles:
    def test_identical_profiles(self) -> None:
        a = profile_server(FASTMCP_SERVER)
        b = profile_server(FASTMCP_SERVER)
        comp = compare_profiles(a, b)
        assert comp.fingerprint_match is True
        assert comp.complexity_delta == 0
        assert comp.tools_added == []
        assert comp.tools_removed == []

    def test_different_profiles(self) -> None:
        a = profile_server(FASTMCP_SERVER)
        b = profile_server(MCP_PYTHON_SDK_SERVER)
        comp = compare_profiles(a, b)
        assert comp.fingerprint_match is False

    def test_tools_diff(self) -> None:
        a = ServerProfile(name="a", tools=[
            ToolInfo(name="read_file"),
            ToolInfo(name="write_file"),
        ], fingerprint="aaa")
        b = ServerProfile(name="b", tools=[
            ToolInfo(name="read_file"),
            ToolInfo(name="delete_file"),
        ], fingerprint="bbb")
        comp = compare_profiles(a, b)
        assert "delete_file" in comp.tools_added
        assert "write_file" in comp.tools_removed
        assert "read_file" in comp.tools_common

    def test_resources_diff(self) -> None:
        a = ServerProfile(name="a", resources=[
            ResourceInfo(uri="docs://a"),
        ], fingerprint="aaa")
        b = ServerProfile(name="b", resources=[
            ResourceInfo(uri="docs://a"),
            ResourceInfo(uri="docs://b"),
        ], fingerprint="bbb")
        comp = compare_profiles(a, b)
        assert "docs://b" in comp.resources_added
        assert "docs://a" in comp.resources_common

    def test_framework_diff(self) -> None:
        a = ServerProfile(
            name="a",
            framework=FrameworkDetection(name="FastMCP"),
            fingerprint="aaa",
        )
        b = ServerProfile(
            name="b",
            framework=FrameworkDetection(name="mcp-python-sdk"),
            fingerprint="bbb",
        )
        comp = compare_profiles(a, b)
        fw_diff = next(d for d in comp.diffs if d.aspect == "framework")
        assert fw_diff.status == "changed"

    def test_complexity_delta(self) -> None:
        a = ServerProfile(name="a", complexity_score=10, fingerprint="aaa")
        b = ServerProfile(name="b", complexity_score=25, fingerprint="bbb")
        comp = compare_profiles(a, b)
        assert comp.complexity_delta == 15

    def test_timestamp_set(self) -> None:
        a = ServerProfile(name="a", fingerprint="a")
        b = ServerProfile(name="b", fingerprint="b")
        comp = compare_profiles(a, b)
        assert comp.timestamp

    def test_dependency_diff(self) -> None:
        a = ServerProfile(name="a", dependencies=["mcp", "fastmcp"], fingerprint="a")
        b = ServerProfile(name="b", dependencies=["mcp", "anthropic"], fingerprint="b")
        comp = compare_profiles(a, b)
        dep_diff = [d for d in comp.diffs if d.aspect == "dependencies"]
        assert len(dep_diff) == 1
        assert dep_diff[0].status == "changed"


# ===========================================================================
# Format functions
# ===========================================================================

class TestFormatProfileJson:
    def test_valid_json(self) -> None:
        p = profile_server(FASTMCP_SERVER)
        j = format_profile_json(p)
        data = json.loads(j)
        assert "name" in data
        assert "tools" in data
        assert "fingerprint" in data

    def test_roundtrip(self) -> None:
        p = profile_server(FASTMCP_SERVER)
        data = json.loads(format_profile_json(p))
        assert data["name"] == p.name


class TestFormatProfileText:
    def test_header(self) -> None:
        p = profile_server(FASTMCP_SERVER)
        text = format_profile_text(p)
        assert "MCP SERVER PROFILE" in text

    def test_tools_listed(self) -> None:
        p = profile_server(FASTMCP_SERVER)
        text = format_profile_text(p)
        assert "read_file" in text

    def test_fingerprint_shown(self) -> None:
        p = profile_server(FASTMCP_SERVER)
        text = format_profile_text(p)
        assert p.fingerprint in text

    def test_auth_mark(self) -> None:
        p = profile_server(FASTMCP_SERVER)
        text = format_profile_text(p)
        assert "[AUTH]" in text


class TestFormatComparisonJson:
    def test_valid_json(self) -> None:
        a = profile_server(FASTMCP_SERVER)
        b = profile_server(MCP_PYTHON_SDK_SERVER)
        j = format_comparison_json(compare_profiles(a, b))
        data = json.loads(j)
        assert "diffs" in data
        assert "tools_added" in data


class TestFormatComparisonText:
    def test_header(self) -> None:
        a = profile_server(FASTMCP_SERVER)
        b = profile_server(MCP_PYTHON_SDK_SERVER)
        text = format_comparison_text(compare_profiles(a, b))
        assert "PROFILE COMPARISON" in text

    def test_shows_delta(self) -> None:
        a = ServerProfile(name="a", complexity_score=5, fingerprint="a")
        b = ServerProfile(name="b", complexity_score=20, fingerprint="b")
        text = format_comparison_text(compare_profiles(a, b))
        assert "Complexity Delta:" in text
