"""Tests for Pydantic response/request models."""

from __future__ import annotations

from datetime import datetime, timezone
from uuid import UUID, uuid4

import pytest
from pydantic import ValidationError

from mcpgateway_sdk.models import (
    ApiKey,
    ApiKeyCreate,
    AttachResult,
    ExecResult,
    PaginatedResponse,
    Sandbox,
    SandboxCreate,
    SandboxFile,
    Server,
    Session,
    Skill,
    SkillCatalogEntry,
    ToolExecuteResult,
    ToolLookup,
    ToolSearchResult,
)


# ---------------------------------------------------------------------------
# Fixtures — factory helpers for realistic API payloads
# ---------------------------------------------------------------------------

_NOW = datetime(2026, 3, 29, 12, 0, 0, tzinfo=timezone.utc)
_UUID1 = uuid4()
_UUID2 = uuid4()
_UUID3 = uuid4()


@pytest.fixture
def server_payload() -> dict:
    """Realistic server response from the API."""
    return {
        "id": str(_UUID1),
        "name": "mcp-fetch",
        "display_name": "Fetch",
        "description": "HTTP fetch server",
        "icon_url": "https://example.com/icon.png",
        "type": "stdio",
        "status": "running",
        "config": {"command": "npx", "args": ["-y", "@anthropic/mcp-fetch"]},
        "source": "catalog",
        "source_doc_url": None,
        "credential_mode": "none",
        "exposure_mode": "all",
        "portal_visible": True,
        "portal_category": "utilities",
        "portal_tags": ["fetch", "http"],
        "tool_count": 3,
        "last_synced_at": _NOW.isoformat(),
        "last_error": None,
        "oauth_config": None,
        "created_at": _NOW.isoformat(),
        "updated_at": _NOW.isoformat(),
        "mcp_endpoint_url": "/sse/mcp-fetch",
    }


@pytest.fixture
def tool_lookup_payload() -> dict:
    return {
        "tool_id": str(_UUID1),
        "tool_name": "fetch_url",
        "description": "Fetch a URL and return its contents",
        "server_id": str(_UUID2),
        "server_name": "mcp-fetch",
        "input_schema": {
            "type": "object",
            "properties": {"url": {"type": "string"}},
            "required": ["url"],
        },
    }


@pytest.fixture
def tool_search_payload() -> dict:
    return {
        "tool_id": str(_UUID1),
        "tool_name": "fetch_url",
        "description": "Fetch a URL",
        "server_id": str(_UUID2),
        "server_name": "mcp-fetch",
        "score": 0.92,
        "input_schema": {"type": "object", "properties": {"url": {"type": "string"}}},
    }


@pytest.fixture
def tool_execute_payload() -> dict:
    return {
        "result": {"content": "Hello, world!"},
        "server_id": str(_UUID1),
        "tool_name": "fetch_url",
        "duration_ms": 245,
        "error": None,
    }


@pytest.fixture
def skill_payload() -> dict:
    return {
        "id": str(_UUID1),
        "name": "pdf-reader",
        "description": "Reads PDF documents",
        "source_type": "builtin",
        "source_url": None,
        "mcp_server_id": str(_UUID2),
        "mcp_server_name": "mcp-pdf-reader",
        "skill_metadata": {"version": "1.0.0", "author": "MCPGateway"},
        "package_size_bytes": 12345,
        "portal_visible": True,
        "portal_category": "document-readers",
        "portal_tags": ["pdf", "reader"],
        "skill_md_content": "# PDF Reader\nReads PDFs.",
        "created_at": _NOW.isoformat(),
        "updated_at": _NOW.isoformat(),
    }


@pytest.fixture
def skill_catalog_payload() -> dict:
    return {
        "name": "pdf-reader",
        "description": "Reads PDF documents",
        "source": "builtin",
        "source_url": "https://example.com/pdf-reader",
        "author": "MCPGateway",
        "version": "1.0.0",
        "tags": ["pdf", "reader"],
        "license": "MIT",
        "files": ["skill.md", "main.py"],
        "metadata": {"category": "document-readers"},
    }


@pytest.fixture
def attach_result_payload() -> dict:
    return {
        "attachment_id": "att_abc123",
        "tools_registered": ["read_pdf", "extract_text"],
        "server_id": str(_UUID1),
        "skill_id": str(_UUID2),
    }


@pytest.fixture
def sandbox_payload() -> dict:
    return {
        "id": str(_UUID1),
        "user_id": str(_UUID2),
        "user_name": "testuser",
        "status": "running",
        "image": "mcpgateway/sandbox-runtime:latest",
        "container_id": "abc123def456",
        "workspace_path": "/workspace",
        "created_at": _NOW.isoformat(),
        "last_activity_at": _NOW.isoformat(),
        "stopped_at": None,
        "memory_mb": 1024,
        "cpu_cores": 2.0,
        "disk_mb": 2048,
        "idle_timeout_seconds": 3600,
        "network_enabled": True,
        "exec_count": 5,
        "session_key": "sess_xyz",
    }


@pytest.fixture
def exec_result_payload() -> dict:
    return {
        "exit_code": 0,
        "stdout": "Hello from sandbox\n",
        "stderr": "",
        "duration_ms": 120,
    }


@pytest.fixture
def sandbox_file_payload() -> dict:
    return {
        "name": "main.py",
        "path": "/workspace/main.py",
        "size": 256,
        "is_directory": False,
        "modified_at": _NOW.isoformat(),
    }


@pytest.fixture
def api_key_payload() -> dict:
    return {
        "id": str(_UUID1),
        "name": "test-key",
        "description": "A test API key",
        "key_prefix": "mk_live_abc",
        "environment": "live",
        "scopes": ["tools:read", "tools:execute"],
        "rate_limit_rpm": 120,
        "is_active": True,
        "created_at": _NOW.isoformat(),
        "updated_at": _NOW.isoformat(),
        "expires_at": None,
        "last_used_at": None,
        "last_used_ip": None,
        "key": None,
    }


# ---------------------------------------------------------------------------
# Server
# ---------------------------------------------------------------------------


@pytest.fixture
def session_payload() -> dict:
    """Realistic session response from the API."""
    return {
        "id": str(_UUID1),
        "server_id": str(_UUID2),
        "bundle_id": str(_UUID3),
        "process_state": {"step": 1},
        "external_sessions": {"srv1": "remote-abc"},
        "last_accessed_at": _NOW.isoformat(),
        "expires_at": _NOW.isoformat(),
        "is_active": True,
        "is_expired": False,
        "is_idle": False,
        "created_at": _NOW.isoformat(),
        "allowed_tool_names": ["PREFIX__tool_a", "PREFIX__tool_b"],
    }


# ---------------------------------------------------------------------------
# Session
# ---------------------------------------------------------------------------


class TestSession:
    def test_parse_full_response(self, session_payload: dict) -> None:
        session = Session(**session_payload)
        assert session.id == _UUID1
        assert session.server_id == _UUID2
        assert session.bundle_id == _UUID3
        assert session.process_state == {"step": 1}
        assert session.external_sessions == {"srv1": "remote-abc"}
        assert session.allowed_tool_names == ["PREFIX__tool_a", "PREFIX__tool_b"]
        assert session.is_active is True
        assert session.is_expired is False
        assert session.is_idle is False

    def test_parse_minimal_response(self) -> None:
        """Session with only required fields."""
        data = {
            "id": str(_UUID1),
            "created_at": _NOW.isoformat(),
            "last_accessed_at": _NOW.isoformat(),
            "expires_at": _NOW.isoformat(),
        }
        session = Session(**data)
        assert session.id == _UUID1
        assert session.server_id is None
        assert session.bundle_id is None
        assert session.process_state == {}
        assert session.external_sessions == {}
        assert session.allowed_tool_names is None
        assert session.is_active is True
        assert session.is_expired is False
        assert session.is_idle is False
        assert session.created_at == _NOW
        assert session.last_accessed_at == _NOW
        assert session.expires_at == _NOW

    def test_extra_fields_ignored(self, session_payload: dict) -> None:
        session_payload["brand_new_field"] = "future-proof"
        session = Session(**session_payload)
        assert session.id == _UUID1
        assert not hasattr(session, "brand_new_field")

    def test_null_allowed_tool_names(self) -> None:
        data = {
            "id": str(_UUID1),
            "created_at": _NOW.isoformat(),
            "last_accessed_at": _NOW.isoformat(),
            "expires_at": _NOW.isoformat(),
            "allowed_tool_names": None,
        }
        session = Session(**data)
        assert session.allowed_tool_names is None


# ---------------------------------------------------------------------------
# Server
# ---------------------------------------------------------------------------


class TestServer:
    def test_parse_full_response(self, server_payload: dict) -> None:
        server = Server(**server_payload)
        assert server.id == _UUID1
        assert server.name == "mcp-fetch"
        assert server.display_name == "Fetch"
        assert server.type == "stdio"
        assert server.status == "running"
        assert server.tool_count == 3
        assert server.portal_tags == ["fetch", "http"]
        assert server.mcp_endpoint_url == "/sse/mcp-fetch"

    def test_parse_minimal_response(self) -> None:
        """Server with only required fields."""
        data = {"id": str(_UUID1), "name": "minimal-server", "type": "sse"}
        server = Server(**data)
        assert server.id == _UUID1
        assert server.name == "minimal-server"
        assert server.display_name == ""
        assert server.status == "unknown"
        assert server.tool_count == 0
        assert server.config == {}
        assert server.portal_tags == []

    def test_extra_fields_ignored(self, server_payload: dict) -> None:
        server_payload["brand_new_field"] = "future-proof"
        server_payload["another_unknown"] = 42
        server = Server(**server_payload)
        assert server.name == "mcp-fetch"
        assert not hasattr(server, "brand_new_field")


# ---------------------------------------------------------------------------
# Tool models
# ---------------------------------------------------------------------------


class TestToolLookup:
    def test_parse_with_input_schema(self, tool_lookup_payload: dict) -> None:
        tool = ToolLookup(**tool_lookup_payload)
        assert tool.tool_id == _UUID1
        assert tool.tool_name == "fetch_url"
        assert tool.server_id == _UUID2
        assert "properties" in tool.input_schema
        assert tool.input_schema["properties"]["url"]["type"] == "string"

    def test_defaults(self) -> None:
        data = {
            "tool_id": str(_UUID1),
            "tool_name": "test",
            "server_id": str(_UUID2),
        }
        tool = ToolLookup(**data)
        assert tool.description == ""
        assert tool.server_name == ""
        assert tool.input_schema == {}


class TestToolSearchResult:
    def test_parse_with_score(self, tool_search_payload: dict) -> None:
        result = ToolSearchResult(**tool_search_payload)
        assert result.tool_id == _UUID1
        assert result.score == 0.92
        assert result.server_name == "mcp-fetch"

    def test_default_score_is_zero(self) -> None:
        data = {
            "tool_id": str(_UUID1),
            "tool_name": "test",
            "server_id": str(_UUID2),
        }
        result = ToolSearchResult(**data)
        assert result.score == 0.0

    def test_extra_fields_ignored(self, tool_search_payload: dict) -> None:
        tool_search_payload["embedding_model"] = "text-embedding-3-small"
        result = ToolSearchResult(**tool_search_payload)
        assert not hasattr(result, "embedding_model")


class TestToolExecuteResult:
    def test_parse_success(self, tool_execute_payload: dict) -> None:
        result = ToolExecuteResult(**tool_execute_payload)
        assert result.result == {"content": "Hello, world!"}
        assert result.server_id == _UUID1
        assert result.tool_name == "fetch_url"
        assert result.duration_ms == 245
        assert result.error is None

    def test_parse_error(self) -> None:
        data = {
            "result": None,
            "server_id": str(_UUID1),
            "tool_name": "fetch_url",
            "duration_ms": 50,
            "error": "Connection refused",
        }
        result = ToolExecuteResult(**data)
        assert result.error == "Connection refused"
        assert result.result is None


# ---------------------------------------------------------------------------
# Skill models
# ---------------------------------------------------------------------------


class TestSkill:
    def test_parse_full(self, skill_payload: dict) -> None:
        skill = Skill(**skill_payload)
        assert skill.id == _UUID1
        assert skill.name == "pdf-reader"
        assert skill.skill_metadata["version"] == "1.0.0"
        assert skill.mcp_server_id == _UUID2
        assert skill.portal_tags == ["pdf", "reader"]

    def test_defaults(self) -> None:
        data = {"id": str(_UUID1), "name": "minimal"}
        skill = Skill(**data)
        assert skill.description is None
        assert skill.source_type == ""
        assert skill.skill_metadata == {}
        assert skill.package_size_bytes == 0
        assert skill.portal_tags == []


class TestSkillCatalogEntry:
    def test_parse(self, skill_catalog_payload: dict) -> None:
        entry = SkillCatalogEntry(**skill_catalog_payload)
        assert entry.name == "pdf-reader"
        assert entry.author == "MCPGateway"
        assert entry.version == "1.0.0"
        assert entry.tags == ["pdf", "reader"]
        assert entry.license == "MIT"
        assert entry.files == ["skill.md", "main.py"]

    def test_defaults(self) -> None:
        entry = SkillCatalogEntry(name="test-skill")
        assert entry.description == ""
        assert entry.source == ""
        assert entry.tags == []
        assert entry.files == []
        assert entry.metadata == {}


class TestAttachResult:
    def test_parse(self, attach_result_payload: dict) -> None:
        result = AttachResult(**attach_result_payload)
        assert result.attachment_id == "att_abc123"
        assert result.tools_registered == ["read_pdf", "extract_text"]
        assert result.server_id == _UUID1
        assert result.skill_id == _UUID2


# ---------------------------------------------------------------------------
# Sandbox models
# ---------------------------------------------------------------------------


class TestSandbox:
    def test_parse_full(self, sandbox_payload: dict) -> None:
        sandbox = Sandbox(**sandbox_payload)
        assert sandbox.id == _UUID1
        assert sandbox.status == "running"
        assert sandbox.memory_mb == 1024
        assert sandbox.cpu_cores == 2.0
        assert sandbox.network_enabled is True
        assert sandbox.exec_count == 5
        assert sandbox.session_key == "sess_xyz"

    def test_defaults(self) -> None:
        data = {"id": str(_UUID1)}
        sandbox = Sandbox(**data)
        assert sandbox.status == "unknown"
        assert sandbox.image == ""
        assert sandbox.memory_mb == 512
        assert sandbox.cpu_cores == 1.0
        assert sandbox.disk_mb == 1024
        assert sandbox.idle_timeout_seconds == 1800
        assert sandbox.network_enabled is False
        assert sandbox.exec_count == 0

    def test_extra_fields_ignored(self, sandbox_payload: dict) -> None:
        sandbox_payload["k8s_pod_name"] = "sandbox-abc-xyz"
        sandbox = Sandbox(**sandbox_payload)
        assert not hasattr(sandbox, "k8s_pod_name")


class TestSandboxCreate:
    def test_defaults(self) -> None:
        create = SandboxCreate()
        assert create.image == "mcpgateway/sandbox-runtime:latest"
        assert create.idle_timeout_seconds == 1800
        assert create.memory_mb is None
        assert create.cpu_cores is None
        assert create.disk_mb is None
        assert create.env_vars == {}
        assert create.network_enabled is None
        assert create.session_key is None

    def test_custom_values(self) -> None:
        create = SandboxCreate(
            image="custom:v1",
            idle_timeout_seconds=600,
            memory_mb=2048,
            cpu_cores=4.0,
            env_vars={"FOO": "bar"},
            network_enabled=True,
            session_key="my-session",
        )
        assert create.image == "custom:v1"
        assert create.memory_mb == 2048
        assert create.env_vars == {"FOO": "bar"}


class TestExecResult:
    def test_parse(self, exec_result_payload: dict) -> None:
        result = ExecResult(**exec_result_payload)
        assert result.exit_code == 0
        assert result.stdout == "Hello from sandbox\n"
        assert result.stderr == ""
        assert result.duration_ms == 120

    def test_defaults(self) -> None:
        result = ExecResult()
        assert result.exit_code == 0
        assert result.stdout == ""
        assert result.stderr == ""
        assert result.duration_ms == 0


class TestSandboxFile:
    def test_parse(self, sandbox_file_payload: dict) -> None:
        f = SandboxFile(**sandbox_file_payload)
        assert f.name == "main.py"
        assert f.path == "/workspace/main.py"
        assert f.size == 256
        assert f.is_directory is False
        assert f.modified_at is not None

    def test_directory(self) -> None:
        data = {"name": "src", "path": "/workspace/src", "is_directory": True}
        f = SandboxFile(**data)
        assert f.is_directory is True
        assert f.size == 0

    def test_missing_path_defaults_to_empty(self) -> None:
        """Backend may omit path from file listing items."""
        data = {"name": "script.py", "size": 128, "is_directory": False}
        f = SandboxFile(**data)
        assert f.name == "script.py"
        assert f.path == ""
        assert f.size == 128


# ---------------------------------------------------------------------------
# API Key models
# ---------------------------------------------------------------------------


class TestApiKey:
    def test_parse_full(self, api_key_payload: dict) -> None:
        key = ApiKey(**api_key_payload)
        assert key.id == _UUID1
        assert key.name == "test-key"
        assert key.key_prefix == "mk_live_abc"
        assert key.is_active is True
        assert key.scopes == ["tools:read", "tools:execute"]
        assert key.rate_limit_rpm == 120

    def test_creation_response_with_key(self) -> None:
        data = {
            "id": str(_UUID1),
            "name": "new-key",
            "key_prefix": "mk_live_xyz",
            "key": "mk_live_xyz_full_secret_token_here",
            "is_active": True,
            "created_at": _NOW.isoformat(),
        }
        key = ApiKey(**data)
        assert key.key == "mk_live_xyz_full_secret_token_here"

    def test_extra_fields_ignored(self, api_key_payload: dict) -> None:
        api_key_payload["usage_count_30d"] = 1500
        key = ApiKey(**api_key_payload)
        assert not hasattr(key, "usage_count_30d")


class TestApiKeyCreate:
    def test_defaults(self) -> None:
        create = ApiKeyCreate(name="my-key")
        assert create.name == "my-key"
        assert create.description is None
        assert create.environment == "live"
        assert create.scopes is None
        assert create.rate_limit_rpm == 60
        assert create.expires_in_days is None
        assert create.expires_at is None

    def test_custom_values(self) -> None:
        create = ApiKeyCreate(
            name="prod-key",
            description="Production API key",
            environment="live",
            scopes=["tools:execute", "servers:read"],
            rate_limit_rpm=1000,
            expires_in_days=90,
        )
        assert create.scopes == ["tools:execute", "servers:read"]
        assert create.rate_limit_rpm == 1000

    def test_name_required(self) -> None:
        with pytest.raises(ValidationError):
            ApiKeyCreate()  # type: ignore[call-arg]


# ---------------------------------------------------------------------------
# PaginatedResponse
# ---------------------------------------------------------------------------


class TestPaginatedResponse:
    def test_parse(self) -> None:
        data = {
            "items": [{"id": "1", "name": "a"}, {"id": "2", "name": "b"}],
            "total": 50,
            "offset": 0,
            "limit": 20,
        }
        page = PaginatedResponse(**data)
        assert len(page.items) == 2
        assert page.total == 50
        assert page.offset == 0
        assert page.limit == 20

    def test_defaults(self) -> None:
        data = {"items": [], "total": 0}
        page = PaginatedResponse(**data)
        assert page.offset == 0
        assert page.limit == 100

    def test_extra_fields_ignored(self) -> None:
        data = {
            "items": [],
            "total": 0,
            "next_cursor": "abc",
            "has_more": True,
        }
        page = PaginatedResponse(**data)
        assert not hasattr(page, "next_cursor")
        assert not hasattr(page, "has_more")
