# 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.

"""Tests for shared utilities (exceptions and message handling)."""

import json
import unittest

from adp_sdk.shared import (
    ADPError,
    ExecutionFailedError,
    InternalError,
    InvalidParamsError,
    InvalidRequestError,
    MethodNotFoundError,
    ParseError,
    ResourceNotFoundError,
    UnauthorizedError,
    ValidationFailedError,
    create_error_from_exception,
    create_error_response,
    create_result_response,
    deserialize_json,
    deserialize_request,
    get_exception_for_error_code,
    serialize_message,
)
from adp_sdk.types import (
    EXECUTION_FAILED,
    INTERNAL_ERROR,
    INVALID_PARAMS,
    INVALID_REQUEST,
    METHOD_NOT_FOUND,
    PARSE_ERROR,
    RESOURCE_NOT_FOUND,
    UNAUTHORIZED,
    VALIDATION_FAILED,
    JSONRPCError,
    JSONRPCRequest,
    JSONRPCResultResponse,
)

_ERROR_SUBCLASSES: list[tuple[type[ADPError], int, str]] = [
    (ParseError, PARSE_ERROR, "Parse error"),
    (InvalidRequestError, INVALID_REQUEST, "Invalid request"),
    (MethodNotFoundError, METHOD_NOT_FOUND, "Method not found"),
    (InvalidParamsError, INVALID_PARAMS, "Invalid params"),
    (InternalError, INTERNAL_ERROR, "Internal error"),
    (ResourceNotFoundError, RESOURCE_NOT_FOUND, "Resource not found"),
    (ValidationFailedError, VALIDATION_FAILED, "Validation failed"),
    (UnauthorizedError, UNAUTHORIZED, "Unauthorized"),
    (ExecutionFailedError, EXECUTION_FAILED, "Execution failed"),
]


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


class TestExceptions(unittest.TestCase):
    """Tests for ADP exception classes."""

    def test_adp_error_defaults(self) -> None:
        err = ADPError("something went wrong")
        self.assertEqual(str(err), "something went wrong")
        self.assertEqual(err.code, INTERNAL_ERROR)
        self.assertIsNone(err.data)

    def test_adp_error_custom_code(self) -> None:
        err = ADPError("oops", code=42, data={"key": "val"})
        self.assertEqual(err.code, 42)
        self.assertEqual(err.data, {"key": "val"})

    def test_adp_error_repr(self) -> None:
        err = ADPError("msg", code=1)
        self.assertIn("ADPError", repr(err))
        self.assertIn("code=1", repr(err))

    def test_error_subclasses(self) -> None:
        for cls, expected_code, default_msg in _ERROR_SUBCLASSES:
            with self.subTest(cls=cls.__name__):
                err = cls(default_msg)
                self.assertIsInstance(err, ADPError)
                self.assertEqual(err.code, expected_code)
                self.assertEqual(str(err), default_msg)

    def test_error_subclass_custom_message(self) -> None:
        err = ResourceNotFoundError("users table not found", data={"id": "users"})
        self.assertEqual(str(err), "users table not found")
        self.assertEqual(err.code, RESOURCE_NOT_FOUND)
        self.assertEqual(err.data, {"id": "users"})


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


class TestGetExceptionForErrorCode(unittest.TestCase):
    """Tests for error code to exception mapping."""

    def test_known_error_codes(self) -> None:
        cases: list[tuple[int, type[ADPError]]] = [
            (PARSE_ERROR, ParseError),
            (INVALID_REQUEST, InvalidRequestError),
            (METHOD_NOT_FOUND, MethodNotFoundError),
            (INVALID_PARAMS, InvalidParamsError),
            (INTERNAL_ERROR, InternalError),
            (RESOURCE_NOT_FOUND, ResourceNotFoundError),
            (VALIDATION_FAILED, ValidationFailedError),
            (UNAUTHORIZED, UnauthorizedError),
            (EXECUTION_FAILED, ExecutionFailedError),
        ]
        for code, expected_cls in cases:
            with self.subTest(code=code):
                exc = get_exception_for_error_code(code, "test msg")
                self.assertIsInstance(exc, expected_cls)
                self.assertEqual(str(exc), "test msg")

    def test_unknown_error_code(self) -> None:
        exc = get_exception_for_error_code(99999, "unknown error")
        self.assertIs(type(exc), ADPError)
        self.assertEqual(str(exc), "unknown error")
        self.assertEqual(exc.code, 99999)

    def test_with_data(self) -> None:
        exc = get_exception_for_error_code(PARSE_ERROR, "bad json", data={"line": 1})
        self.assertIsInstance(exc, ParseError)
        self.assertEqual(exc.data, {"line": 1})


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


class TestSerializeMessage(unittest.TestCase):
    """Tests for message serialization."""

    def test_serialize_request(self) -> None:
        req = JSONRPCRequest(id=1, method="adp.ping", params={})
        result = serialize_message(req)
        data = json.loads(result)
        self.assertEqual(data["jsonrpc"], "2.0")
        self.assertEqual(data["id"], 1)
        self.assertEqual(data["method"], "adp.ping")

    def test_serialize_response(self) -> None:
        resp = JSONRPCResultResponse(id=1, result={"status": "ok"})
        result = serialize_message(resp)
        data = json.loads(result)
        self.assertEqual(data["id"], 1)
        self.assertEqual(data["result"]["status"], "ok")

    def test_serialize_excludes_none(self) -> None:
        req = JSONRPCRequest(id=1, method="adp.ping")
        result = serialize_message(req)
        data = json.loads(result)
        self.assertNotIn("params", data)


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


class TestDeserializeRequest(unittest.TestCase):
    """Tests for request deserialization."""

    def test_valid_request(self) -> None:
        raw = '{"jsonrpc":"2.0","id":1,"method":"adp.ping","params":{}}'
        req = deserialize_request(raw)
        self.assertEqual(req.id, 1)
        self.assertEqual(req.method, "adp.ping")

    def test_valid_request_bytes(self) -> None:
        raw = b'{"jsonrpc":"2.0","id":1,"method":"adp.ping","params":{}}'
        req = deserialize_request(raw)
        self.assertEqual(req.id, 1)

    def test_invalid_json(self) -> None:
        with self.assertRaises(ParseError):
            deserialize_request("{invalid")

    def test_invalid_utf8_bytes(self) -> None:
        with self.assertRaises(ParseError):
            deserialize_request(b"\xff\xfe{}")

    def test_invalid_structure(self) -> None:
        with self.assertRaises(ParseError):
            deserialize_request('{"jsonrpc":"2.0"}')


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


class TestDeserializeJson(unittest.TestCase):
    """Tests for generic JSON deserialization."""

    def test_valid_json(self) -> None:
        data = deserialize_json('{"key": "value"}')
        self.assertEqual(data, {"key": "value"})

    def test_valid_json_bytes(self) -> None:
        data = deserialize_json(b'{"key": "value"}')
        self.assertEqual(data, {"key": "value"})

    def test_invalid_json(self) -> None:
        with self.assertRaises(ParseError):
            deserialize_json("not json")

    def test_invalid_utf8_bytes(self) -> None:
        with self.assertRaises(ParseError):
            deserialize_json(b"\xff\xfe{}")


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


class TestCreateResultResponse(unittest.TestCase):
    """Tests for creating result responses."""

    def test_create_result_response(self) -> None:
        resp = create_result_response(42, {"data": [1, 2, 3]})
        self.assertEqual(resp.id, 42)
        self.assertEqual(resp.result, {"data": [1, 2, 3]})


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


class TestCreateErrorResponse(unittest.TestCase):
    """Tests for creating error responses."""

    def test_from_adp_error(self) -> None:
        err = ResourceNotFoundError("not found", data={"id": "x"})
        resp = create_error_response(1, err)
        self.assertEqual(resp.id, 1)
        self.assertEqual(resp.error.code, RESOURCE_NOT_FOUND)
        self.assertEqual(resp.error.message, "not found")
        self.assertEqual(resp.error.data, {"id": "x"})

    def test_from_jsonrpc_error(self) -> None:
        err = JSONRPCError(code=-32600, message="bad request")
        resp = create_error_response(2, err)
        self.assertEqual(resp.id, 2)
        self.assertEqual(resp.error.code, -32600)

    def test_null_request_id(self) -> None:
        err = ParseError("parse error")
        resp = create_error_response(None, err)
        self.assertIsNone(resp.id)


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


class TestCreateErrorFromException(unittest.TestCase):
    """Tests for creating error responses from exception details."""

    def test_basic(self) -> None:
        resp = create_error_from_exception(1, -32600, "bad request")
        self.assertEqual(resp.id, 1)
        self.assertEqual(resp.error.code, -32600)
        self.assertEqual(resp.error.message, "bad request")
        self.assertIsNone(resp.error.data)

    def test_with_data(self) -> None:
        resp = create_error_from_exception(1, -32600, "bad", data={"detail": "x"})
        self.assertEqual(resp.error.data, {"detail": "x"})
