# Copyright 2026 Datastrato, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Integration tests for the ADP SDK.

Uses a mock ADP server script (Python subprocess) that responds to
the full protocol lifecycle: initialize → discover → describe → validate → execute.
"""

import sys
import textwrap
import unittest

from adp_sdk import ClientSession, ResourceNotFoundError, stdio_client
from adp_sdk.transports.stdio import StdioClientTransport
from adp_sdk.types.common import PredicateGroup
from adp_sdk.types.intents import QueryIntent

MOCK_SERVER_SCRIPT = textwrap.dedent("""\
    import json
    import sys

    PROTOCOL_VERSION = "2026-01-20"

    RESOURCES = [
        {
            "resourceId": "com.test:users",
            "version": 1,
            "intentClasses": ["QUERY", "LOOKUP"],
            "description": "Test user data",
        }
    ]

    USAGE_CONTRACT = {
        "fields": [
            {"fieldId": "id", "type": "INTEGER"},
            {"fieldId": "name", "type": "STRING"},
        ],
        "capabilities": {},
    }

    ROWS = [
        {"id": 1, "name": "Alice"},
        {"id": 2, "name": "Bob"},
    ]

    def handle(request):
        method = request["method"]
        rid = request["id"]

        if method == "adp.initialize":
            return {
                "jsonrpc": "2.0",
                "id": rid,
                "result": {
                    "protocolVersion": PROTOCOL_VERSION,
                    "capabilities": {},
                    "serverInfo": {"name": "mock-server", "version": "0.1.0"},
                },
            }
        elif method == "adp.ping":
            return {"jsonrpc": "2.0", "id": rid, "result": {}}
        elif method == "adp.discover":
            return {
                "jsonrpc": "2.0",
                "id": rid,
                "result": {"resources": RESOURCES},
            }
        elif method == "adp.describe":
            params = request.get("params", {})
            res_id = params.get("resourceId")
            if res_id != "com.test:users":
                return {
                    "jsonrpc": "2.0",
                    "id": rid,
                    "error": {"code": -32001, "message": f"Resource not found: {res_id}"},
                }
            return {
                "jsonrpc": "2.0",
                "id": rid,
                "result": {
                    "resourceId": res_id,
                    "version": 1,
                    "intentClass": params.get("intentClass", "QUERY"),
                    "usageContract": USAGE_CONTRACT,
                },
            }
        elif method == "adp.validate":
            return {"jsonrpc": "2.0", "id": rid, "result": {"valid": True}}
        elif method == "adp.execute":
            return {
                "jsonrpc": "2.0",
                "id": rid,
                "result": {"results": ROWS},
            }
        else:
            return {
                "jsonrpc": "2.0",
                "id": rid,
                "error": {"code": -32601, "message": f"Method not found: {method}"},
            }

    for line in sys.stdin:
        line = line.strip()
        if not line:
            continue
        request = json.loads(line)
        response = handle(request)
        sys.stdout.write(json.dumps(response) + "\\n")
        sys.stdout.flush()
""")


def _make_query_intent(resource_id: str = "com.test:users") -> QueryIntent:
    return QueryIntent(
        intentClass="QUERY",
        resourceId=resource_id,
        predicates=PredicateGroup(op="AND", predicates=[]),
    )


# ============================================================================


class TestIntegration(unittest.IsolatedAsyncioTestCase):
    """End-to-end integration tests with a mock ADP server."""

    async def test_full_lifecycle(self) -> None:
        """Test the complete initialize → discover → describe → validate → execute flow."""
        async with stdio_client(sys.executable, ["-c", MOCK_SERVER_SCRIPT]) as session:
            self.assertTrue(session.initialized)
            self.assertIsNotNone(session.server_info)
            self.assertEqual(session.server_info.name, "mock-server")  # type: ignore[union-attr]

            # Ping
            await session.ping()

            # Discover
            discovery = await session.discover()
            self.assertEqual(len(discovery.resources), 1)
            self.assertEqual(discovery.resources[0].resource_id, "com.test:users")
            self.assertIn("QUERY", discovery.resources[0].intent_classes)

            # Describe
            contract = await session.describe("com.test:users", "QUERY")
            self.assertEqual(contract.resource_id, "com.test:users")
            self.assertEqual(len(contract.usage_contract.fields), 2)
            field_ids = [f.field_id for f in contract.usage_contract.fields]
            self.assertIn("id", field_ids)
            self.assertIn("name", field_ids)

            # Validate
            intent = _make_query_intent()
            validation = await session.validate(intent)
            self.assertTrue(validation.valid)

            # Execute
            result = await session.execute(intent)
            self.assertEqual(len(result.results), 2)
            self.assertEqual(result.results[0]["name"], "Alice")
            self.assertEqual(result.results[1]["name"], "Bob")

    async def test_server_error(self) -> None:
        """Test that server errors are properly mapped to exceptions."""
        async with stdio_client(sys.executable, ["-c", MOCK_SERVER_SCRIPT]) as session:
            with self.assertRaises(ResourceNotFoundError):
                await session.describe("com.test:nonexistent", "QUERY")

    async def test_stdio_client_convenience(self) -> None:
        """Test the stdio_client convenience function."""
        async with stdio_client(sys.executable, ["-c", MOCK_SERVER_SCRIPT]) as session:
            self.assertIsInstance(session, ClientSession)
            self.assertTrue(session.initialized)
            result = await session.discover()
            self.assertGreater(len(result.resources), 0)

    async def test_direct_transport_usage(self) -> None:
        """Test using StdioClientTransport directly with ClientSession."""
        transport = StdioClientTransport(sys.executable, ["-c", MOCK_SERVER_SCRIPT])
        async with ClientSession(transport) as session:
            result = await session.discover()
            self.assertEqual(len(result.resources), 1)

    async def test_multiple_operations(self) -> None:
        """Test making multiple sequential operations."""
        async with stdio_client(sys.executable, ["-c", MOCK_SERVER_SCRIPT]) as session:
            # Multiple discovers
            r1 = await session.discover()
            r2 = await session.discover()
            self.assertEqual(len(r1.resources), len(r2.resources))

            # Multiple pings
            await session.ping()
            await session.ping()
            await session.ping()
