"""Tests for MCP Integration Test Generator."""

from __future__ import annotations

import json
import textwrap

import pytest

from mcpaudit.test_generator import (
    DetectedPrompt,
    DetectedResource,
    DetectedTool,
    GeneratedTest,
    ServerProfile,
    GeneratedSuite,
    _detect_framework,
    _detect_prompts,
    _detect_resources,
    _detect_server_name,
    _detect_tools_ast,
    _detect_tools_regex,
    _generate_fixture,
    _generate_prompt_tests,
    _generate_resource_tests,
    _generate_tool_tests,
    _sanitize_name,
    detect_server_components,
    format_suite_json,
    format_suite_text,
    generate_test_suite,
    generate_tests,
)


# --- DetectedTool ---

class TestDetectedTool:
    def test_creation(self):
        t = DetectedTool(name="read_file", line=10, parameters=["path"])
        assert t.name == "read_file"
        assert t.line == 10

    def test_to_dict(self):
        t = DetectedTool(name="t", line=1, parameters=["a", "b"], has_async=True)
        d = t.to_dict()
        assert d["name"] == "t"
        assert d["has_async"] is True
        assert len(d["parameters"]) == 2

    def test_defaults(self):
        t = DetectedTool(name="t", line=1)
        assert t.parameters == []
        assert t.description == ""
        assert t.has_async is False


# --- DetectedResource ---

class TestDetectedResource:
    def test_creation(self):
        r = DetectedResource(name="docs", uri_pattern="docs://readme")
        assert r.name == "docs"
        assert r.uri_pattern == "docs://readme"

    def test_to_dict(self):
        r = DetectedResource(name="r", uri_pattern="x://y", line=5)
        d = r.to_dict()
        assert d["uri_pattern"] == "x://y"


# --- DetectedPrompt ---

class TestDetectedPrompt:
    def test_creation(self):
        p = DetectedPrompt(name="greeting", arguments=["name"])
        assert p.name == "greeting"
        assert p.arguments == ["name"]

    def test_to_dict(self):
        p = DetectedPrompt(name="p", line=3, arguments=["x"])
        d = p.to_dict()
        assert d["name"] == "p"
        assert d["arguments"] == ["x"]


# --- ServerProfile ---

class TestServerProfile:
    def test_empty(self):
        sp = ServerProfile()
        assert sp.total_components == 0

    def test_with_components(self):
        sp = ServerProfile(
            tools=[DetectedTool(name="t", line=1)],
            resources=[DetectedResource(name="r")],
            prompts=[DetectedPrompt(name="p")],
        )
        assert sp.total_components == 3

    def test_to_dict(self):
        sp = ServerProfile(framework="fastmcp", server_name="test")
        d = sp.to_dict()
        assert d["framework"] == "fastmcp"
        assert d["server_name"] == "test"


# --- GeneratedSuite ---

class TestTestSuiteModel:
    def test_empty(self):
        ts = GeneratedSuite()
        assert ts.total_tests == 0

    def test_to_dict(self):
        ts = GeneratedSuite(total_tests=5)
        d = ts.to_dict()
        assert d["total_tests"] == 5


# --- GeneratedTest ---

class TestGeneratedTestModel:
    def test_to_dict(self):
        gt = GeneratedTest(name="test_x", category="invocation",
                           component="tool_a", code="def test_x(): pass")
        d = gt.to_dict()
        assert d["name"] == "test_x"
        assert d["category"] == "invocation"


# --- _detect_framework ---

class TestDetectFramework:
    def test_fastmcp_import(self):
        assert _detect_framework("from fastmcp import FastMCP") == "fastmcp"

    def test_mcp_server_import(self):
        assert _detect_framework("from mcp.server.fastmcp import FastMCP") == "fastmcp"

    def test_mcp_sdk(self):
        assert _detect_framework("from mcp.server import Server") == "mcp-sdk"

    def test_decorator_pattern(self):
        assert _detect_framework("@server.tool\ndef f(): pass") == "fastmcp"

    def test_unknown(self):
        assert _detect_framework("print('hello')") == "unknown"


# --- _detect_server_name ---

class TestDetectServerName:
    def test_fastmcp_constructor(self):
        code = 'app = FastMCP("my-server")'
        assert _detect_server_name(code) == "my-server"

    def test_name_attribute(self):
        code = "name = 'test-server'"
        assert _detect_server_name(code) == "test-server"

    def test_fallback(self):
        assert _detect_server_name("x = 1") == "mcp_server"


# --- _sanitize_name ---

class TestSanitizeName:
    def test_simple(self):
        assert _sanitize_name("read_file") == "read_file"

    def test_with_special_chars(self):
        assert _sanitize_name("read-file.v2") == "read_file_v2"

    def test_uppercase(self):
        assert _sanitize_name("ReadFile") == "readfile"


# --- _detect_tools_ast ---

class TestDetectToolsAST:
    def test_decorator_tool(self):
        code = textwrap.dedent("""\
        @tool
        def read_file(path: str):
            return open(path).read()
        """)
        tools = _detect_tools_ast(code)
        assert len(tools) >= 1
        assert tools[0].name == "read_file"

    def test_server_tool_decorator(self):
        code = textwrap.dedent("""\
        @server.tool()
        def write_file(path: str, content: str):
            pass
        """)
        tools = _detect_tools_ast(code)
        assert len(tools) >= 1

    def test_async_tool(self):
        code = textwrap.dedent("""\
        @tool
        async def async_tool(query: str):
            return "result"
        """)
        tools = _detect_tools_ast(code)
        assert len(tools) >= 1
        assert tools[0].has_async is True

    def test_no_tools(self):
        code = "def regular_function():\n    pass"
        tools = _detect_tools_ast(code)
        assert len(tools) == 0

    def test_parameters_extracted(self):
        code = "@tool\ndef my_tool(name: str, count: int):\n    pass"
        tools = _detect_tools_ast(code)
        assert len(tools) >= 1
        assert "name" in tools[0].parameters
        assert "count" in tools[0].parameters

    def test_self_excluded(self):
        code = textwrap.dedent("""\
        class Server:
            @tool
            def my_tool(self, data: str):
                pass
        """)
        tools = _detect_tools_ast(code)
        if tools:
            assert "self" not in tools[0].parameters

    def test_add_tool_pattern(self):
        code = textwrap.dedent("""\
        def my_handler():
            pass
        server.add_tool(my_handler)
        """)
        tools = _detect_tools_ast(code)
        assert any(t.name == "my_handler" for t in tools)


# --- _detect_tools_regex ---

class TestDetectToolsRegex:
    def test_decorator_pattern(self):
        code = "@tool\ndef read_file(path):\n    pass"
        tools = _detect_tools_regex(code)
        assert len(tools) >= 1

    def test_add_tool_pattern(self):
        code = "server.add_tool(handler)"
        tools = _detect_tools_regex(code)
        assert any(t.name == "handler" for t in tools)

    def test_syntax_error_fallback(self):
        code = "@tool\ndef broken_func(x):\n    pass"
        # This should work via regex since it's syntactically valid
        tools = _detect_tools_regex(code)
        assert len(tools) >= 1


# --- _detect_resources ---

class TestDetectResources:
    def test_decorator_resource(self):
        code = '@resource("docs://readme")\ndef get_readme():\n    pass'
        resources = _detect_resources(code)
        assert len(resources) >= 1
        assert resources[0].uri_pattern == "docs://readme"

    def test_server_resource(self):
        code = '@server.resource("config://settings")\ndef get_settings():\n    pass'
        resources = _detect_resources(code)
        assert len(resources) >= 1

    def test_add_resource_pattern(self):
        code = 'server.add_resource("data://users")'
        resources = _detect_resources(code)
        assert len(resources) >= 1

    def test_no_resources(self):
        code = "def f():\n    pass"
        resources = _detect_resources(code)
        assert len(resources) == 0


# --- _detect_prompts ---

class TestDetectPrompts:
    def test_decorator_prompt(self):
        code = "@prompt\ndef greeting(name: str):\n    pass"
        prompts = _detect_prompts(code)
        assert len(prompts) >= 1
        assert prompts[0].name == "greeting"

    def test_prompt_arguments(self):
        code = "@prompt\ndef review(code: str, language: str):\n    pass"
        prompts = _detect_prompts(code)
        assert len(prompts) >= 1
        assert "code" in prompts[0].arguments
        assert "language" in prompts[0].arguments

    def test_no_prompts(self):
        code = "def f():\n    pass"
        prompts = _detect_prompts(code)
        assert len(prompts) == 0


# --- detect_server_components ---

class TestDetectServerComponents:
    def test_full_server(self):
        code = textwrap.dedent("""\
        from fastmcp import FastMCP
        app = FastMCP("test-server")

        @app.tool()
        def read_file(path: str):
            return open(path).read()

        @app.resource("docs://readme")
        def get_readme():
            return "README content"

        @app.prompt()
        def greeting(name: str):
            return f"Hello {name}"
        """)
        profile = detect_server_components(code)
        assert profile.framework == "fastmcp"
        assert profile.server_name == "test-server"
        assert len(profile.tools) >= 1
        assert len(profile.resources) >= 1
        assert len(profile.prompts) >= 1
        assert profile.total_components >= 3

    def test_empty_code(self):
        profile = detect_server_components("")
        assert profile.total_components == 0
        assert profile.framework == "unknown"


# --- Test generation ---

class TestGenerateToolTests:
    def test_basic_tool(self):
        tool = DetectedTool(name="read_file", line=1, parameters=["path"])
        tests = _generate_tool_tests(tool, "fastmcp")
        assert len(tests) >= 3
        assert any(t.category == "invocation" for t in tests)
        assert any(t.category == "error_handling" for t in tests)
        assert any(t.category == "permissions" for t in tests)

    def test_async_tool(self):
        tool = DetectedTool(name="async_tool", line=1, has_async=True)
        tests = _generate_tool_tests(tool, "fastmcp")
        assert any("async" in t.code for t in tests)

    def test_tool_with_params(self):
        tool = DetectedTool(name="search", line=1, parameters=["query", "limit"])
        tests = _generate_tool_tests(tool, "fastmcp")
        assert any(t.category == "edge_case" for t in tests)

    def test_tool_no_params(self):
        tool = DetectedTool(name="status", line=1)
        tests = _generate_tool_tests(tool, "fastmcp")
        assert all(t.category != "edge_case" for t in tests)

    def test_test_names_valid(self):
        tool = DetectedTool(name="my_tool", line=1)
        tests = _generate_tool_tests(tool, "fastmcp")
        for t in tests:
            assert t.name.startswith("test_")


class TestGenerateResourceTests:
    def test_basic_resource(self):
        res = DetectedResource(name="readme", uri_pattern="docs://readme")
        tests = _generate_resource_tests(res, "fastmcp")
        assert len(tests) >= 3
        assert any(t.category == "invocation" for t in tests)
        assert any(t.category == "error_handling" for t in tests)
        assert any(t.category == "permissions" for t in tests)

    def test_resource_uri_in_tests(self):
        res = DetectedResource(name="config", uri_pattern="config://settings")
        tests = _generate_resource_tests(res, "fastmcp")
        assert any("config://settings" in t.code for t in tests)


class TestGeneratePromptTests:
    def test_basic_prompt(self):
        p = DetectedPrompt(name="greeting", arguments=["name"])
        tests = _generate_prompt_tests(p, "fastmcp")
        assert len(tests) >= 3

    def test_prompt_with_args(self):
        p = DetectedPrompt(name="review", arguments=["code", "lang"])
        tests = _generate_prompt_tests(p, "fastmcp")
        assert any("code" in t.code for t in tests)


# --- _generate_fixture ---

class TestGenerateFixture:
    def test_fixture_content(self):
        profile = ServerProfile(framework="fastmcp", server_name="test")
        fixture = _generate_fixture(profile)
        assert "mcp_client" in fixture
        assert "fastmcp" in fixture

    def test_fixture_has_import(self):
        profile = ServerProfile()
        fixture = _generate_fixture(profile)
        assert "import pytest" in fixture


# --- generate_tests (main) ---

class TestGenerateTests:
    def test_generates_valid_python(self):
        code = textwrap.dedent("""\
        from fastmcp import FastMCP
        app = FastMCP("demo")

        @app.tool()
        def hello(name: str):
            return f"Hello {name}"
        """)
        output = generate_tests(code)
        assert "import pytest" in output
        assert "test_hello" in output
        # Should be parseable Python
        compile(output, "<test>", "exec")

    def test_empty_server(self):
        output = generate_tests("x = 1")
        assert "import pytest" in output

    def test_multiple_tools(self):
        code = textwrap.dedent("""\
        @tool
        def tool_a(x: str):
            pass

        @tool
        def tool_b(y: int):
            pass
        """)
        output = generate_tests(code)
        assert "tool_a" in output
        assert "tool_b" in output

    def test_with_resources_and_prompts(self):
        code = textwrap.dedent("""\
        from fastmcp import FastMCP
        app = FastMCP("full-server")

        @app.tool()
        def search(query: str):
            pass

        @app.resource("data://items")
        def get_items():
            pass

        @app.prompt()
        def summarize(text: str):
            pass
        """)
        output = generate_tests(code)
        assert "search" in output
        assert "get_items" in output or "items" in output
        assert "summarize" in output


# --- generate_test_suite ---

class TestGenerateTestSuite:
    def test_suite_structure(self):
        code = "@tool\ndef my_tool(x: str):\n    pass"
        suite = generate_test_suite(code)
        assert suite.total_tests > 0
        assert len(suite.tests) == suite.total_tests
        assert suite.profile.framework is not None

    def test_suite_categories(self):
        code = "@tool\ndef my_tool(x: str):\n    pass"
        suite = generate_test_suite(code)
        categories = {t.category for t in suite.tests}
        assert "invocation" in categories
        assert "error_handling" in categories

    def test_suite_empty_code(self):
        suite = generate_test_suite("x = 1")
        assert suite.total_tests == 0


# --- Formatting ---

class TestFormatting:
    def test_format_text(self):
        code = "@tool\ndef my_tool(x: str):\n    pass"
        suite = generate_test_suite(code)
        text = format_suite_text(suite)
        assert "Test Generator" in text
        assert "Tools" in text

    def test_format_json(self):
        code = "@tool\ndef my_tool(x: str):\n    pass"
        suite = generate_test_suite(code)
        j = format_suite_json(suite)
        data = json.loads(j)
        assert "total_tests" in data
        assert "profile" in data

    def test_format_text_empty(self):
        suite = GeneratedSuite()
        text = format_suite_text(suite)
        assert "0" in text

    def test_format_json_roundtrip(self):
        code = "@tool\ndef t(a: str):\n    pass"
        suite = generate_test_suite(code)
        j = format_suite_json(suite)
        data = json.loads(j)
        assert data["total_tests"] == suite.total_tests
