"""
CASE client-side validation tests.

Tests that validation happens in the resource layer (before transport)
using a stub transport. Mirrors the TS SDK validation.unit.test.ts.
"""

from __future__ import annotations

from typing import Any

import pytest

from timeback_case.resources.associations import AssociationsResource
from timeback_case.resources.documents import DocumentsResource
from timeback_case.resources.items import ItemsResource
from timeback_case.resources.packages import PackagesResource
from timeback_common import InputValidationError


class _StubPaths:
    base = "/ims/case/v1p1"


class _StubTransport:
    """Minimal stub transport that records requests without making HTTP calls."""

    def __init__(self) -> None:
        self.request_count = 0
        self.paths = _StubPaths()

    async def get(self, _path: str, **_kwargs: Any) -> dict[str, Any]:
        self.request_count += 1
        _mock_doc = {
            "sourcedId": "mock-id",
            "title": "Mock",
            "uri": "urn:mock",
            "creator": "Mock Org",
            "lastChangeDateTime": "2024-01-01T00:00:00Z",
            "CFPackageURI": {"sourcedId": "mock-id", "title": "Mock", "uri": "urn:mock"},
        }
        return {
            "CFDocument": _mock_doc,
            "CFItem": {
                "sourcedId": "mock-id",
                "fullStatement": "Mock",
                "CFItemType": "Standard",
                "uri": "urn:mock",
                "lastChangeDateTime": "2024-01-01T00:00:00Z",
            },
            "CFAssociation": {
                "sourcedId": "mock-id",
                "associationType": "isChildOf",
                "uri": "urn:mock",
                "originNodeURI": {"sourcedId": "a", "title": "A", "uri": "urn:a"},
                "destinationNodeURI": {"sourcedId": "b", "title": "B", "uri": "urn:b"},
                "lastChangeDateTime": "2024-01-01T00:00:00Z",
            },
            "CFPackage": {
                "sourcedId": "mock-id",
                "CFDocument": _mock_doc,
            },
            "CFPackageWithGroups": {
                "sourcedId": "mock-id",
                "CFDocument": _mock_doc,
            },
        }

    async def post(self, _path: str, _data: Any, **_kwargs: Any) -> dict[str, Any]:
        self.request_count += 1
        return {
            "success": True,
            "message": "Package uploaded",
            "result": {
                "documentId": "doc-001",
                "stats": {"documents": 1, "items": 1, "associations": 1},
                "success": True,
            },
        }

    async def put(self, _path: str, _data: Any, **_kwargs: Any) -> dict[str, Any]:
        self.request_count += 1
        return {
            "success": True,
            "message": "Package updated",
            "result": {
                "documentId": "doc-001",
                "stats": {"documents": 1, "items": 1, "associations": 1},
                "success": True,
            },
        }

    async def close(self) -> None:
        pass


def _valid_package_input() -> dict[str, Any]:
    return {
        "CFDocument": {
            "identifier": "doc-001",
            "uri": "urn:doc-001",
            "lastChangeDateTime": "2024-01-01T00:00:00Z",
            "title": "Test Framework",
            "creator": "Test Org",
        },
        "CFItems": [
            {
                "identifier": "item-001",
                "uri": "urn:item-001",
                "lastChangeDateTime": "2024-01-01T00:00:00Z",
                "fullStatement": "Student can demonstrate test concepts",
            },
        ],
        "CFAssociations": [
            {
                "identifier": "assoc-001",
                "uri": "urn:assoc-001",
                "lastChangeDateTime": "2024-01-01T00:00:00Z",
                "associationType": "isChildOf",
                "originNodeURI": {
                    "title": "Test Item",
                    "identifier": "item-001",
                    "uri": "urn:item-001",
                },
                "destinationNodeURI": {
                    "title": "Test Doc",
                    "identifier": "doc-001",
                    "uri": "urn:doc-001",
                },
            },
        ],
    }


# ─────────────────────────────────────────────────────────────────────────────
# sourcedId validation on get methods
# ─────────────────────────────────────────────────────────────────────────────


class TestSourcedIdValidation:
    """Tests that empty sourcedId is rejected before reaching the transport."""

    @pytest.mark.asyncio
    async def test_documents_get_validates_sourced_id(self) -> None:
        transport = _StubTransport()
        resource = DocumentsResource(transport)  # type: ignore[arg-type]
        with pytest.raises(InputValidationError):
            await resource.get("")
        assert transport.request_count == 0

    @pytest.mark.asyncio
    async def test_items_get_validates_sourced_id(self) -> None:
        transport = _StubTransport()
        resource = ItemsResource(transport)  # type: ignore[arg-type]
        with pytest.raises(InputValidationError):
            await resource.get("")
        assert transport.request_count == 0

    @pytest.mark.asyncio
    async def test_associations_get_validates_sourced_id(self) -> None:
        transport = _StubTransport()
        resource = AssociationsResource(transport)  # type: ignore[arg-type]
        with pytest.raises(InputValidationError):
            await resource.get("")
        assert transport.request_count == 0

    @pytest.mark.asyncio
    async def test_packages_get_validates_sourced_id(self) -> None:
        transport = _StubTransport()
        resource = PackagesResource(transport)  # type: ignore[arg-type]
        with pytest.raises(InputValidationError):
            await resource.get("")
        assert transport.request_count == 0

    @pytest.mark.asyncio
    async def test_packages_get_groups_validates_sourced_id(self) -> None:
        transport = _StubTransport()
        resource = PackagesResource(transport)  # type: ignore[arg-type]
        with pytest.raises(InputValidationError):
            await resource.get_groups("")
        assert transport.request_count == 0

    @pytest.mark.asyncio
    async def test_packages_update_validates_sourced_id(self) -> None:
        transport = _StubTransport()
        resource = PackagesResource(transport)  # type: ignore[arg-type]
        with pytest.raises(InputValidationError):
            await resource.update("", _valid_package_input())
        assert transport.request_count == 0


# ─────────────────────────────────────────────────────────────────────────────
# Package input validation
# ─────────────────────────────────────────────────────────────────────────────


class TestPackageInputValidation:
    """Tests that invalid package inputs are rejected before reaching the transport."""

    @pytest.mark.asyncio
    async def test_rejects_empty_document_identifier(self) -> None:
        transport = _StubTransport()
        resource = PackagesResource(transport)  # type: ignore[arg-type]
        data = _valid_package_input()
        data["CFDocument"]["identifier"] = ""
        with pytest.raises(InputValidationError):
            await resource.create(data)
        assert transport.request_count == 0

    @pytest.mark.asyncio
    async def test_rejects_empty_document_title(self) -> None:
        transport = _StubTransport()
        resource = PackagesResource(transport)  # type: ignore[arg-type]
        data = _valid_package_input()
        data["CFDocument"]["title"] = ""
        with pytest.raises(InputValidationError):
            await resource.create(data)
        assert transport.request_count == 0

    @pytest.mark.asyncio
    async def test_rejects_empty_item_full_statement(self) -> None:
        transport = _StubTransport()
        resource = PackagesResource(transport)  # type: ignore[arg-type]
        data = _valid_package_input()
        data["CFItems"][0]["fullStatement"] = ""
        with pytest.raises(InputValidationError):
            await resource.create(data)
        assert transport.request_count == 0

    @pytest.mark.asyncio
    async def test_rejects_empty_association_origin_node_uri_identifier(self) -> None:
        transport = _StubTransport()
        resource = PackagesResource(transport)  # type: ignore[arg-type]
        data = _valid_package_input()
        data["CFAssociations"][0]["originNodeURI"]["identifier"] = ""
        with pytest.raises(InputValidationError):
            await resource.create(data)
        assert transport.request_count == 0


# ─────────────────────────────────────────────────────────────────────────────
# Valid inputs pass validation
# ─────────────────────────────────────────────────────────────────────────────


class TestValidInputs:
    """Tests that valid inputs pass validation and reach the transport."""

    @pytest.mark.asyncio
    async def test_packages_create_passes_with_valid_input(self) -> None:
        transport = _StubTransport()
        resource = PackagesResource(transport)  # type: ignore[arg-type]
        result = await resource.create(_valid_package_input())
        assert transport.request_count == 1
        assert result.success is True

    @pytest.mark.asyncio
    async def test_documents_get_passes_with_valid_sourced_id(self) -> None:
        transport = _StubTransport()
        resource = DocumentsResource(transport)  # type: ignore[arg-type]
        await resource.get("valid-sourced-id")
        assert transport.request_count == 1
