"""Tests for MCP Protocol Validator."""

from __future__ import annotations

import json
import tempfile
from pathlib import Path

import pytest

from mcpaudit.protocol_validator import (
    ProtocolIssue,
    ProtocolValidationResult,
    _compute_compliance_score,
    extract_mcp_definitions,
    format_protocol_json,
    format_protocol_text,
    validate_prompt,
    validate_protocol,
    validate_protocol_directory,
    validate_protocol_file,
    validate_resource,
    validate_tool,
)


# ---------------------------------------------------------------------------
# ProtocolIssue dataclass
# ---------------------------------------------------------------------------


class TestProtocolIssue:
    def test_to_dict(self):
        issue = ProtocolIssue(
            category="tool",
            severity="ERROR",
            message="Missing name",
            element_name="test_tool",
            file="test.json",
            line=5,
            field="name",
        )
        d = issue.to_dict()
        assert d["category"] == "tool"
        assert d["severity"] == "ERROR"
        assert d["message"] == "Missing name"
        assert d["element_name"] == "test_tool"
        assert d["file"] == "test.json"
        assert d["line"] == 5
        assert d["field"] == "name"

    def test_default_values(self):
        issue = ProtocolIssue(
            category="schema",
            severity="WARNING",
            message="Test",
        )
        assert issue.element_name == ""
        assert issue.file == ""
        assert issue.line == 0
        assert issue.field == ""


# ---------------------------------------------------------------------------
# ProtocolValidationResult
# ---------------------------------------------------------------------------


class TestProtocolValidationResult:
    def test_exit_code_clean(self):
        result = ProtocolValidationResult()
        assert result.exit_code == 0

    def test_exit_code_warning(self):
        result = ProtocolValidationResult(
            issues=[ProtocolIssue(category="tool", severity="WARNING", message="test")]
        )
        assert result.exit_code == 1

    def test_exit_code_error(self):
        result = ProtocolValidationResult(
            issues=[ProtocolIssue(category="tool", severity="ERROR", message="test")]
        )
        assert result.exit_code == 2

    def test_to_dict(self):
        result = ProtocolValidationResult(
            tools_checked=3,
            resources_checked=2,
            prompts_checked=1,
            compliance_score=85,
            file="test.json",
        )
        d = result.to_dict()
        assert d["tools_checked"] == 3
        assert d["resources_checked"] == 2
        assert d["prompts_checked"] == 1
        assert d["compliance_score"] == 85
        assert d["total_issues"] == 0

    def test_to_dict_counts(self):
        result = ProtocolValidationResult(
            issues=[
                ProtocolIssue(category="tool", severity="ERROR", message="e1"),
                ProtocolIssue(category="tool", severity="WARNING", message="w1"),
                ProtocolIssue(category="tool", severity="INFO", message="i1"),
            ]
        )
        d = result.to_dict()
        assert d["error_count"] == 1
        assert d["warning_count"] == 1
        assert d["info_count"] == 1
        assert d["total_issues"] == 3


# ---------------------------------------------------------------------------
# validate_tool
# ---------------------------------------------------------------------------


class TestValidateTool:
    def test_valid_tool(self):
        tool = {
            "name": "get_weather",
            "description": "Get current weather for a location",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "location": {"type": "string", "description": "City name"}
                },
                "required": ["location"],
            },
        }
        issues = validate_tool(tool)
        assert len(issues) == 0

    def test_missing_name(self):
        tool = {"description": "A tool"}
        issues = validate_tool(tool)
        assert any(i.severity == "ERROR" and "name" in i.message.lower() for i in issues)

    def test_empty_name(self):
        tool = {"name": "", "description": "A tool"}
        issues = validate_tool(tool)
        assert any("name" in i.message.lower() for i in issues)

    def test_non_string_name(self):
        tool = {"name": 123, "description": "A tool"}
        issues = validate_tool(tool)
        assert any("string" in i.message.lower() for i in issues)

    def test_long_name(self):
        tool = {"name": "a" * 100, "description": "A tool with a very long name"}
        issues = validate_tool(tool)
        assert any("exceeds" in i.message.lower() for i in issues)

    def test_unusual_name_chars(self):
        tool = {"name": "my tool with spaces!", "description": "A tool"}
        issues = validate_tool(tool)
        assert any("unusual" in i.message.lower() for i in issues)

    def test_missing_description(self):
        tool = {"name": "test_tool", "inputSchema": {"type": "object"}}
        issues = validate_tool(tool)
        assert any("description" in i.message.lower() and "no description" in i.message.lower() for i in issues)

    def test_empty_description(self):
        tool = {"name": "test_tool", "description": ""}
        issues = validate_tool(tool)
        assert any("no description" in i.message.lower() for i in issues)

    def test_non_string_description(self):
        tool = {"name": "test_tool", "description": 123}
        issues = validate_tool(tool)
        assert any("string" in i.message.lower() and "description" in i.field for i in issues)

    def test_short_description(self):
        tool = {"name": "test_tool", "description": "Short"}
        issues = validate_tool(tool)
        assert any("short" in i.message.lower() for i in issues)

    def test_missing_input_schema(self):
        tool = {"name": "test_tool", "description": "A valid tool"}
        issues = validate_tool(tool)
        assert any("inputSchema" in i.message for i in issues)

    def test_non_dict_input_schema(self):
        tool = {"name": "test_tool", "description": "A valid tool", "inputSchema": "bad"}
        issues = validate_tool(tool)
        assert any("object" in i.message.lower() and "inputSchema" in i.field for i in issues)

    def test_missing_schema_type(self):
        tool = {
            "name": "test_tool",
            "description": "A valid tool",
            "inputSchema": {"properties": {}},
        }
        issues = validate_tool(tool)
        assert any("type" in i.message.lower() and "inputSchema.type" in i.field for i in issues)

    def test_wrong_schema_type(self):
        tool = {
            "name": "test_tool",
            "description": "A valid tool",
            "inputSchema": {"type": "array"},
        }
        issues = validate_tool(tool)
        assert any("should be 'object'" in i.message for i in issues)

    def test_missing_properties(self):
        tool = {
            "name": "test_tool",
            "description": "A valid tool",
            "inputSchema": {"type": "object"},
        }
        issues = validate_tool(tool)
        assert any("properties" in i.message.lower() for i in issues)

    def test_non_dict_properties(self):
        tool = {
            "name": "test_tool",
            "description": "A valid tool",
            "inputSchema": {"type": "object", "properties": "bad"},
        }
        issues = validate_tool(tool)
        assert any("properties must be an object" in i.message for i in issues)

    def test_property_missing_type(self):
        tool = {
            "name": "test_tool",
            "description": "A valid tool",
            "inputSchema": {
                "type": "object",
                "properties": {"name": {"description": "Name"}},
            },
        }
        issues = validate_tool(tool)
        assert any("missing 'type'" in i.message for i in issues)

    def test_property_missing_description(self):
        tool = {
            "name": "test_tool",
            "description": "A valid tool",
            "inputSchema": {
                "type": "object",
                "properties": {"name": {"type": "string"}},
            },
        }
        issues = validate_tool(tool)
        assert any("no description" in i.message.lower() and "name" in i.field for i in issues)

    def test_property_non_dict(self):
        tool = {
            "name": "test_tool",
            "description": "A valid tool",
            "inputSchema": {
                "type": "object",
                "properties": {"name": "string"},
            },
        }
        issues = validate_tool(tool)
        assert any("must be an object" in i.message for i in issues)

    def test_required_not_in_properties(self):
        tool = {
            "name": "test_tool",
            "description": "A valid tool",
            "inputSchema": {
                "type": "object",
                "properties": {"name": {"type": "string"}},
                "required": ["name", "missing_field"],
            },
        }
        issues = validate_tool(tool)
        assert any("missing_field" in i.message and "not in properties" in i.message for i in issues)

    def test_required_non_array(self):
        tool = {
            "name": "test_tool",
            "description": "A valid tool",
            "inputSchema": {
                "type": "object",
                "properties": {},
                "required": "name",
            },
        }
        issues = validate_tool(tool)
        assert any("must be an array" in i.message for i in issues)

    def test_valid_required_field(self):
        tool = {
            "name": "test_tool",
            "description": "A valid tool description here",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string", "description": "The name"},
                },
                "required": ["name"],
            },
        }
        issues = validate_tool(tool)
        assert len(issues) == 0


# ---------------------------------------------------------------------------
# validate_resource
# ---------------------------------------------------------------------------


class TestValidateResource:
    def test_valid_resource(self):
        resource = {
            "name": "config",
            "uri": "file:///etc/config.json",
            "description": "Configuration file",
            "mimeType": "application/json",
        }
        issues = validate_resource(resource)
        assert len(issues) == 0

    def test_missing_uri(self):
        resource = {"name": "config", "description": "Config"}
        issues = validate_resource(resource)
        assert any("uri" in i.message.lower() for i in issues)

    def test_non_string_uri(self):
        resource = {"name": "config", "uri": 123, "description": "Config"}
        issues = validate_resource(resource)
        assert any("string" in i.message.lower() and "uri" in i.field for i in issues)

    def test_malformed_uri(self):
        resource = {"name": "config", "uri": "no_scheme_no_path", "description": "Config"}
        issues = validate_resource(resource)
        assert any("malformed" in i.message.lower() for i in issues)

    def test_missing_name(self):
        resource = {"uri": "file:///test", "description": "Test"}
        issues = validate_resource(resource)
        assert any("name" in i.message.lower() and i.severity == "ERROR" for i in issues)

    def test_missing_description(self):
        resource = {"name": "config", "uri": "file:///test"}
        issues = validate_resource(resource)
        assert any("no description" in i.message.lower() for i in issues)

    def test_non_string_mime_type(self):
        resource = {
            "name": "config",
            "uri": "file:///test",
            "description": "Config",
            "mimeType": 123,
        }
        issues = validate_resource(resource)
        assert any("mimeType" in i.message for i in issues)

    def test_invalid_mime_type(self):
        resource = {
            "name": "config",
            "uri": "file:///test",
            "description": "Config",
            "mimeType": "invalid",
        }
        issues = validate_resource(resource)
        assert any("invalid" in i.message.lower() for i in issues)

    def test_valid_path_uri(self):
        resource = {"name": "logs", "uri": "/var/log/app.log", "description": "App logs"}
        issues = validate_resource(resource)
        malformed = [i for i in issues if "malformed" in i.message.lower()]
        assert len(malformed) == 0


# ---------------------------------------------------------------------------
# validate_prompt
# ---------------------------------------------------------------------------


class TestValidatePrompt:
    def test_valid_prompt(self):
        prompt = {
            "name": "greet",
            "description": "Generate a greeting",
            "arguments": [
                {"name": "username", "description": "The user name"},
            ],
            "messages": [
                {"role": "user", "content": "Hello {username}!"},
            ],
        }
        issues = validate_prompt(prompt)
        assert len(issues) == 0

    def test_missing_name(self):
        prompt = {"description": "Test"}
        issues = validate_prompt(prompt)
        assert any("name" in i.message.lower() and i.severity == "ERROR" for i in issues)

    def test_missing_description(self):
        prompt = {"name": "test"}
        issues = validate_prompt(prompt)
        assert any("no description" in i.message.lower() for i in issues)

    def test_arguments_not_array(self):
        prompt = {"name": "test", "description": "Test", "arguments": "bad"}
        issues = validate_prompt(prompt)
        assert any("must be an array" in i.message for i in issues)

    def test_argument_not_dict(self):
        prompt = {"name": "test", "description": "Test", "arguments": ["bad"]}
        issues = validate_prompt(prompt)
        assert any("must be an object" in i.message for i in issues)

    def test_argument_missing_name(self):
        prompt = {
            "name": "test",
            "description": "Test",
            "arguments": [{"description": "An arg"}],
        }
        issues = validate_prompt(prompt)
        assert any("without name" in i.message for i in issues)

    def test_duplicate_argument_names(self):
        prompt = {
            "name": "test",
            "description": "Test",
            "arguments": [
                {"name": "arg1", "description": "First"},
                {"name": "arg1", "description": "Duplicate"},
            ],
        }
        issues = validate_prompt(prompt)
        assert any("duplicate" in i.message.lower() for i in issues)

    def test_argument_missing_description(self):
        prompt = {
            "name": "test",
            "description": "Test",
            "arguments": [{"name": "arg1"}],
        }
        issues = validate_prompt(prompt)
        assert any("no description" in i.message.lower() and "arg1" in i.message for i in issues)

    def test_undeclared_placeholder(self):
        prompt = {
            "name": "test",
            "description": "Test prompt",
            "arguments": [{"name": "name", "description": "Name"}],
            "messages": [
                {"role": "user", "content": "Hello {name}, your id is {user_id}"},
            ],
        }
        issues = validate_prompt(prompt)
        assert any("user_id" in i.message and "not declared" in i.message for i in issues)

    def test_unused_argument(self):
        prompt = {
            "name": "test",
            "description": "Test prompt",
            "arguments": [
                {"name": "name", "description": "Name"},
                {"name": "unused", "description": "Not used"},
            ],
            "messages": [
                {"role": "user", "content": "Hello {name}!"},
            ],
        }
        issues = validate_prompt(prompt)
        assert any("unused" in i.message and "never uses" in i.message for i in issues)

    def test_placeholder_in_content_dict(self):
        prompt = {
            "name": "test",
            "description": "Test prompt",
            "arguments": [{"name": "topic", "description": "Topic"}],
            "messages": [
                {"role": "user", "content": {"type": "text", "text": "Tell me about {topic}"}},
            ],
        }
        issues = validate_prompt(prompt)
        unused = [i for i in issues if "never uses" in i.message]
        assert len(unused) == 0


# ---------------------------------------------------------------------------
# extract_mcp_definitions
# ---------------------------------------------------------------------------


class TestExtractDefinitions:
    def test_json_with_tools(self):
        source = json.dumps({
            "tools": [{"name": "t1", "description": "D", "inputSchema": {"type": "object"}}],
        })
        defs = extract_mcp_definitions(source)
        assert len(defs["tools"]) == 1
        assert defs["tools"][0]["name"] == "t1"

    def test_json_with_resources(self):
        source = json.dumps({
            "resources": [{"name": "r1", "uri": "file:///test"}],
        })
        defs = extract_mcp_definitions(source)
        assert len(defs["resources"]) == 1

    def test_json_with_prompts(self):
        source = json.dumps({
            "prompts": [{"name": "p1", "arguments": []}],
        })
        defs = extract_mcp_definitions(source)
        assert len(defs["prompts"]) == 1

    def test_json_nested_server(self):
        source = json.dumps({
            "server": {
                "tools": [{"name": "t1", "description": "D"}],
                "resources": [{"name": "r1", "uri": "http://test"}],
            }
        })
        defs = extract_mcp_definitions(source)
        assert len(defs["tools"]) == 1
        assert len(defs["resources"]) == 1

    def test_json_array_of_tools(self):
        source = json.dumps([
            {"name": "t1", "inputSchema": {"type": "object"}},
            {"name": "t2", "inputSchema": {"type": "object"}},
        ])
        defs = extract_mcp_definitions(source)
        assert len(defs["tools"]) == 2

    def test_non_json_source(self):
        source = "def my_tool(): pass"
        defs = extract_mcp_definitions(source)
        assert defs["tools"] == []
        assert defs["resources"] == []
        assert defs["prompts"] == []


# ---------------------------------------------------------------------------
# _compute_compliance_score
# ---------------------------------------------------------------------------


class TestComplianceScore:
    def test_perfect_score(self):
        score = _compute_compliance_score([], 5)
        assert score == 100

    def test_no_elements(self):
        score = _compute_compliance_score([], 0)
        assert score == 100

    def test_errors_reduce_score(self):
        issues = [ProtocolIssue(category="tool", severity="ERROR", message="e")]
        score = _compute_compliance_score(issues, 1)
        assert score < 100

    def test_warnings_reduce_less(self):
        errors = [ProtocolIssue(category="tool", severity="ERROR", message="e")]
        warnings = [ProtocolIssue(category="tool", severity="WARNING", message="w")]
        error_score = _compute_compliance_score(errors, 1)
        warning_score = _compute_compliance_score(warnings, 1)
        assert warning_score > error_score

    def test_score_never_below_zero(self):
        issues = [
            ProtocolIssue(category="tool", severity="ERROR", message=f"e{i}")
            for i in range(100)
        ]
        score = _compute_compliance_score(issues, 1)
        assert score >= 0

    def test_score_never_above_100(self):
        score = _compute_compliance_score([], 100)
        assert score <= 100


# ---------------------------------------------------------------------------
# validate_protocol (full integration)
# ---------------------------------------------------------------------------


class TestValidateProtocol:
    def test_valid_full_definition(self):
        source = json.dumps({
            "tools": [
                {
                    "name": "get_weather",
                    "description": "Get weather for a location",
                    "inputSchema": {
                        "type": "object",
                        "properties": {
                            "city": {"type": "string", "description": "City name"},
                        },
                        "required": ["city"],
                    },
                },
            ],
            "resources": [
                {
                    "name": "config",
                    "uri": "file:///config.json",
                    "description": "App configuration",
                    "mimeType": "application/json",
                },
            ],
            "prompts": [
                {
                    "name": "greet",
                    "description": "Greet the user",
                    "arguments": [{"name": "user", "description": "User name"}],
                    "messages": [{"role": "user", "content": "Hello {user}"}],
                },
            ],
        })
        result = validate_protocol(source, "test.json")
        assert result.tools_checked == 1
        assert result.resources_checked == 1
        assert result.prompts_checked == 1
        assert result.compliance_score == 100
        assert len(result.issues) == 0

    def test_multiple_issues(self):
        source = json.dumps({
            "tools": [
                {"description": "No name tool"},
                {"name": "no_desc"},
            ],
        })
        result = validate_protocol(source)
        assert result.tools_checked == 2
        assert len(result.issues) > 0
        assert result.compliance_score < 100

    def test_empty_source(self):
        result = validate_protocol("not json at all")
        assert result.tools_checked == 0
        assert result.compliance_score == 100


# ---------------------------------------------------------------------------
# validate_protocol_file
# ---------------------------------------------------------------------------


class TestValidateProtocolFile:
    def test_valid_file(self, tmp_path):
        data = json.dumps({
            "tools": [
                {
                    "name": "test",
                    "description": "A test tool for testing",
                    "inputSchema": {
                        "type": "object",
                        "properties": {"x": {"type": "string", "description": "X"}},
                    },
                },
            ],
        })
        f = tmp_path / "tools.json"
        f.write_text(data)
        result = validate_protocol_file(str(f))
        assert result.tools_checked == 1

    def test_nonexistent_file(self):
        result = validate_protocol_file("/nonexistent/file.json")
        assert result.compliance_score == 0
        assert any("not found" in i.message.lower() for i in result.issues)


# ---------------------------------------------------------------------------
# validate_protocol_directory
# ---------------------------------------------------------------------------


class TestValidateProtocolDirectory:
    def test_directory_scan(self, tmp_path):
        data = json.dumps({
            "tools": [{"name": "t1", "description": "Tool one for things", "inputSchema": {"type": "object", "properties": {}}}],
        })
        (tmp_path / "a.json").write_text(data)
        (tmp_path / "b.json").write_text(data)
        result = validate_protocol_directory(str(tmp_path))
        assert result.tools_checked == 2

    def test_single_file_fallback(self, tmp_path):
        data = json.dumps({
            "tools": [{"name": "t1", "description": "Tool one for things", "inputSchema": {"type": "object", "properties": {}}}],
        })
        f = tmp_path / "test.json"
        f.write_text(data)
        result = validate_protocol_directory(str(f))
        assert result.tools_checked == 1


# ---------------------------------------------------------------------------
# Output formatters
# ---------------------------------------------------------------------------


class TestFormatProtocolText:
    def test_clean_output(self):
        result = ProtocolValidationResult(compliance_score=100, tools_checked=1)
        text = format_protocol_text(result)
        assert "Protocol Validator" in text
        assert "100" in text
        assert "No protocol issues" in text

    def test_issues_output(self):
        result = ProtocolValidationResult(
            issues=[
                ProtocolIssue(category="tool", severity="ERROR", message="Bad tool"),
                ProtocolIssue(category="tool", severity="WARNING", message="Warn"),
            ],
            compliance_score=70,
        )
        text = format_protocol_text(result)
        assert "Bad tool" in text
        assert "Warn" in text
        assert "[!!]" in text


class TestFormatProtocolJson:
    def test_json_structure(self):
        result = ProtocolValidationResult(
            tools_checked=2,
            compliance_score=85,
        )
        text = format_protocol_json(result)
        data = json.loads(text)
        assert "protocol_validator" in data
        assert data["protocol_validator"]["tools_checked"] == 2
        assert data["protocol_validator"]["compliance_score"] == 85

    def test_json_with_issues(self):
        result = ProtocolValidationResult(
            issues=[ProtocolIssue(category="tool", severity="ERROR", message="test")],
        )
        text = format_protocol_json(result)
        data = json.loads(text)
        assert len(data["protocol_validator"]["issues"]) == 1
