"""Tests for API Mode Server (mcpaudit.api_server).

Tests HTTP server, request parsing, scan handling, endpoint responses,
and format helpers.
"""

from __future__ import annotations

import json
import threading
import time
import urllib.request
import urllib.error

import pytest

from mcpaudit.api_server import (
    ApiResponse,
    McpauditHandler,
    McpauditServer,
    ScanRequest,
    ScanResponse,
    create_api_server,
    format_error_response,
    format_success_response,
    get_routes,
    handle_scan_request,
    parse_scan_request,
)
from mcpaudit import __version__


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

def _get_json(url: str) -> dict:
    """Make a GET request and parse JSON response."""
    req = urllib.request.Request(url)
    with urllib.request.urlopen(req, timeout=5) as resp:
        return json.loads(resp.read().decode("utf-8"))


def _post_json(url: str, data: dict) -> dict:
    """Make a POST request with JSON body and parse JSON response."""
    body = json.dumps(data).encode("utf-8")
    req = urllib.request.Request(
        url,
        data=body,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        return json.loads(e.read().decode("utf-8"))


# ===========================================================================
# ScanRequest
# ===========================================================================

class TestScanRequest:
    def test_default_values(self) -> None:
        sr = ScanRequest()
        assert sr.code == ""
        assert sr.language == "python"
        assert sr.filename == "input.py"

    def test_from_dict(self) -> None:
        sr = ScanRequest.from_dict({
            "code": "print('hi')",
            "language": "python",
            "filename": "test.py",
        })
        assert sr.code == "print('hi')"
        assert sr.filename == "test.py"

    def test_from_dict_defaults(self) -> None:
        sr = ScanRequest.from_dict({})
        assert sr.code == ""
        assert sr.language == "python"

    def test_to_dict(self) -> None:
        sr = ScanRequest(code="x", language="go")
        d = sr.to_dict()
        assert d["code"] == "x"
        assert d["language"] == "go"

    def test_validate_empty_code(self) -> None:
        sr = ScanRequest(code="")
        errors = sr.validate()
        assert any("code" in e for e in errors)

    def test_validate_invalid_language(self) -> None:
        sr = ScanRequest(code="x", language="ruby")
        errors = sr.validate()
        assert any("language" in e.lower() or "Unsupported" in e for e in errors)

    def test_validate_valid(self) -> None:
        sr = ScanRequest(code="print(1)", language="python")
        errors = sr.validate()
        assert errors == []

    def test_validate_severity_threshold(self) -> None:
        sr = ScanRequest(code="x", severity_threshold="invalid")
        errors = sr.validate()
        assert any("severity" in e.lower() for e in errors)

    def test_validate_valid_severity_threshold(self) -> None:
        sr = ScanRequest(code="x", severity_threshold="high")
        errors = sr.validate()
        assert errors == []

    def test_validate_all_languages(self) -> None:
        for lang in ("python", "typescript", "javascript", "go"):
            sr = ScanRequest(code="x", language=lang)
            errors = sr.validate()
            assert not any("language" in e.lower() for e in errors)


# ===========================================================================
# ScanResponse
# ===========================================================================

class TestScanResponse:
    def test_default_success(self) -> None:
        sr = ScanResponse()
        assert sr.success is True
        assert sr.findings == []

    def test_error_response(self) -> None:
        sr = ScanResponse(success=False, error="bad request")
        d = sr.to_dict()
        assert d["success"] is False
        assert d["error"] == "bad request"
        assert "findings" not in d

    def test_success_response(self) -> None:
        sr = ScanResponse(
            success=True,
            findings=[{"rule_id": "CMD-001"}],
            summary={"total_findings": 1},
            scan_time_ms=42.5,
        )
        d = sr.to_dict()
        assert d["success"] is True
        assert d["findings_count"] == 1
        assert d["scan_time_ms"] == 42.5


# ===========================================================================
# ApiResponse
# ===========================================================================

class TestApiResponse:
    def test_success_to_json(self) -> None:
        resp = ApiResponse(status=200, data={"key": "value"})
        j = resp.to_json()
        data = json.loads(j)
        assert data["success"] is True
        assert data["data"]["key"] == "value"

    def test_error_to_json(self) -> None:
        resp = ApiResponse(status=400, error="bad request")
        j = resp.to_json()
        data = json.loads(j)
        assert data["success"] is False
        assert data["error"] == "bad request"


# ===========================================================================
# parse_scan_request
# ===========================================================================

class TestParseScanRequest:
    def test_from_bytes(self) -> None:
        body = b'{"code": "print(1)", "language": "python"}'
        sr = parse_scan_request(body)
        assert sr.code == "print(1)"

    def test_from_string(self) -> None:
        body = '{"code": "x = 1"}'
        sr = parse_scan_request(body)
        assert sr.code == "x = 1"

    def test_invalid_json(self) -> None:
        with pytest.raises(ValueError, match="Invalid JSON"):
            parse_scan_request(b"not json")

    def test_not_a_dict(self) -> None:
        with pytest.raises(ValueError, match="JSON object"):
            parse_scan_request(b"[1, 2, 3]")


# ===========================================================================
# handle_scan_request
# ===========================================================================

class TestHandleScanRequest:
    def test_empty_code(self) -> None:
        req = ScanRequest(code="")
        resp = handle_scan_request(req)
        assert resp.success is False
        assert "code" in resp.error

    def test_invalid_language(self) -> None:
        req = ScanRequest(code="x", language="ruby")
        resp = handle_scan_request(req)
        assert resp.success is False

    def test_valid_python_code(self) -> None:
        req = ScanRequest(
            code="import subprocess\nsubprocess.run(user_input, shell=True)\n",
            language="python",
            filename="test.py",
        )
        resp = handle_scan_request(req)
        assert resp.success is True
        assert resp.scan_time_ms > 0
        assert "total_findings" in resp.summary

    def test_clean_code(self) -> None:
        req = ScanRequest(
            code="x = 1\ny = 2\nprint(x + y)\n",
            language="python",
        )
        resp = handle_scan_request(req)
        assert resp.success is True

    def test_filename_replaced(self) -> None:
        req = ScanRequest(
            code="import subprocess\nsubprocess.run(x, shell=True)\n",
            language="python",
            filename="my_server.py",
        )
        resp = handle_scan_request(req)
        assert resp.success is True
        # Any findings should use our filename, not the temp path
        for f in resp.findings:
            assert f["file"] == "my_server.py"

    def test_severity_threshold_filter(self) -> None:
        req = ScanRequest(
            code="import subprocess\nsubprocess.run(user_input, shell=True)\n",
            language="python",
            severity_threshold="critical",
        )
        resp = handle_scan_request(req)
        assert resp.success is True
        # Only CRITICAL findings should pass
        for f in resp.findings:
            assert f["severity"] == "CRITICAL"

    def test_rules_filter(self) -> None:
        req = ScanRequest(
            code="import subprocess\nsubprocess.run(x, shell=True)\n",
            language="python",
            rules=["NONEXISTENT-RULE"],
        )
        resp = handle_scan_request(req)
        assert resp.success is True
        assert len(resp.findings) == 0  # no matching rules


# ===========================================================================
# format_error_response / format_success_response
# ===========================================================================

class TestFormatHelpers:
    def test_error_response(self) -> None:
        resp = format_error_response(404, "Not found")
        assert resp.status == 404
        assert resp.error == "Not found"

    def test_success_response(self) -> None:
        resp = format_success_response({"key": "val"})
        assert resp.status == 200
        assert resp.data["key"] == "val"


# ===========================================================================
# get_routes
# ===========================================================================

class TestGetRoutes:
    def test_has_all_routes(self) -> None:
        routes = get_routes()
        assert "GET /health" in routes
        assert "GET /version" in routes
        assert "GET /rules" in routes
        assert "GET /compliance" in routes
        assert "POST /scan" in routes

    def test_routes_have_descriptions(self) -> None:
        routes = get_routes()
        for route, desc in routes.items():
            assert len(desc) > 0


# ===========================================================================
# McpauditServer
# ===========================================================================

class TestMcpauditServer:
    def test_create_api_server(self) -> None:
        server = create_api_server("127.0.0.1", 0)
        assert server.host == "127.0.0.1"
        assert not server.is_running

    def test_address(self) -> None:
        server = McpauditServer("localhost", 9999)
        assert server.address == "http://localhost:9999"

    def test_uptime_zero_when_not_running(self) -> None:
        server = McpauditServer()
        assert server.uptime == 0.0

    def test_is_running_default_false(self) -> None:
        server = McpauditServer()
        assert not server.is_running


# ===========================================================================
# Integration Tests (server start/stop + HTTP requests)
# ===========================================================================

@pytest.fixture()
def running_server():
    """Start server on a random port and yield it."""
    # Use port 0 for OS-assigned port
    import socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(("127.0.0.1", 0))
    port = sock.getsockname()[1]
    sock.close()

    server = McpauditServer("127.0.0.1", port)
    server.start(blocking=False)
    time.sleep(0.3)  # give server time to start
    yield server
    server.stop()


class TestServerIntegration:
    def test_health_endpoint(self, running_server: McpauditServer) -> None:
        data = _get_json(f"{running_server.address}/health")
        assert data["success"] is True
        assert data["data"]["status"] == "healthy"
        assert data["data"]["version"] == __version__

    def test_version_endpoint(self, running_server: McpauditServer) -> None:
        data = _get_json(f"{running_server.address}/version")
        assert data["success"] is True
        assert data["data"]["version"] == __version__
        assert data["data"]["name"] == "mcpaudit"

    def test_index_endpoint(self, running_server: McpauditServer) -> None:
        data = _get_json(f"{running_server.address}/")
        assert data["success"] is True
        assert "routes" in data["data"]

    def test_rules_endpoint(self, running_server: McpauditServer) -> None:
        data = _get_json(f"{running_server.address}/rules")
        assert data["success"] is True
        assert "rules" in data["data"]
        assert data["data"]["total"] > 0

    def test_compliance_endpoint(self, running_server: McpauditServer) -> None:
        data = _get_json(f"{running_server.address}/compliance")
        assert data["success"] is True
        assert "frameworks" in data["data"]

    def test_scan_endpoint(self, running_server: McpauditServer) -> None:
        data = _post_json(
            f"{running_server.address}/scan",
            {"code": "x = 1\nprint(x)\n", "language": "python"},
        )
        assert data["success"] is True
        assert "findings_count" in data

    def test_scan_with_findings(self, running_server: McpauditServer) -> None:
        code = (
            "import subprocess\n"
            "def run(cmd):\n"
            "    subprocess.run(cmd, shell=True)\n"
        )
        data = _post_json(
            f"{running_server.address}/scan",
            {"code": code, "language": "python"},
        )
        assert data["success"] is True

    def test_scan_empty_body(self, running_server: McpauditServer) -> None:
        try:
            req = urllib.request.Request(
                f"{running_server.address}/scan",
                data=b"",
                headers={"Content-Type": "application/json"},
                method="POST",
            )
            with urllib.request.urlopen(req, timeout=5) as resp:
                data = json.loads(resp.read().decode("utf-8"))
        except urllib.error.HTTPError as e:
            data = json.loads(e.read().decode("utf-8"))
        assert data["success"] is False

    def test_scan_invalid_json(self, running_server: McpauditServer) -> None:
        try:
            req = urllib.request.Request(
                f"{running_server.address}/scan",
                data=b"not json",
                headers={"Content-Type": "application/json",
                          "Content-Length": "8"},
                method="POST",
            )
            with urllib.request.urlopen(req, timeout=5) as resp:
                data = json.loads(resp.read().decode("utf-8"))
        except urllib.error.HTTPError as e:
            data = json.loads(e.read().decode("utf-8"))
        assert data["success"] is False

    def test_404_endpoint(self, running_server: McpauditServer) -> None:
        try:
            _get_json(f"{running_server.address}/nonexistent")
            assert False, "Should have raised HTTPError"
        except urllib.error.HTTPError as e:
            data = json.loads(e.read().decode("utf-8"))
            assert data["success"] is False

    def test_post_404(self, running_server: McpauditServer) -> None:
        try:
            data = _post_json(f"{running_server.address}/nonexistent", {})
            assert data["success"] is False
        except (ConnectionResetError, urllib.error.URLError):
            pass  # Server may reset connection on unknown POST routes

    def test_context_manager(self) -> None:
        import socket
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.bind(("127.0.0.1", 0))
        port = sock.getsockname()[1]
        sock.close()

        with McpauditServer("127.0.0.1", port) as server:
            assert server.is_running
            data = _get_json(f"{server.address}/health")
            assert data["success"] is True
        assert not server.is_running

    def test_stop_idempotent(self, running_server: McpauditServer) -> None:
        running_server.stop()
        running_server.stop()  # should not raise
        assert not running_server.is_running
