"""Unit tests for oneOf model structure.

With the custom model emitter, oneOf schemas are emitted as tagged-object
models with optional typed fields — no actual_instance wrappers needed.
model_validate() works directly on server JSON.
"""

from __future__ import annotations

import json

import pytest
from pydantic import ValidationError


class TestChunkingConfiguration:
    """Test ChunkingConfiguration oneOf model."""

    def test_recursive_config(self):
        """Recursive chunking config validates and has typed fields."""
        from goodmem.models.chunking_configuration import ChunkingConfiguration

        config = ChunkingConfiguration.model_validate(
            {"recursive": {"chunkSize": 512, "chunkOverlap": 50,
                           "keepStrategy": "KEEP_END", "lengthMeasurement": "CHARACTER_COUNT"}}
        )
        assert config.recursive is not None
        assert config.recursive.chunk_size == 512
        assert config.none is None
        assert config.sentence is None

    def test_none_config(self):
        """No-chunking config validates."""
        from goodmem.models.chunking_configuration import ChunkingConfiguration

        config = ChunkingConfiguration.model_validate({"none": {}})
        assert config.none is not None
        assert config.recursive is None

    def test_roundtrip(self):
        """model_dump(by_alias=True, exclude_unset=True) produces clean JSON."""
        from goodmem.models.chunking_configuration import ChunkingConfiguration

        config = ChunkingConfiguration.model_validate(
            {"recursive": {"chunkSize": 512, "chunkOverlap": 50,
                           "keepStrategy": "KEEP_END", "lengthMeasurement": "CHARACTER_COUNT"}}
        )
        dumped = config.model_dump(by_alias=True, exclude_unset=True, mode="json")
        assert "recursive" in dumped
        assert dumped["recursive"]["chunkSize"] == 512
        # Unset fields excluded
        assert "none" not in dumped
        assert "sentence" not in dumped


class TestRetrievedItem:
    """Test RetrievedItem oneOf model (memory or chunk)."""

    def test_memory_variant(self):
        """RetrievedItem with memory key validates to typed Memory."""
        from goodmem.models.retrieved_item import RetrievedItem

        raw = {
            "memory": {
                "memoryId": "mem-1",
                "spaceId": "sp-1",
                "contentType": "text/plain",
                "processingStatus": "COMPLETED",
                "pageImageStatus": "NONE",
                "pageImageCount": 0,
                "createdAt": 1704067200000,
                "updatedAt": 1704067200000,
                "createdById": "user-1",
                "updatedById": "user-1",
            }
        }
        item = RetrievedItem.model_validate(raw)
        assert item.memory is not None
        assert item.memory.memory_id == "mem-1"
        assert item.chunk is None

    def test_chunk_variant(self):
        """RetrievedItem with chunk key validates to typed ChunkReference."""
        from goodmem.models.retrieved_item import RetrievedItem

        raw = {
            "chunk": {
                "resultSetId": "rs-1",
                "chunk": {
                    "chunkId": "c-1",
                    "memoryId": "mem-1",
                    "chunkSequenceNumber": 0,
                    "chunkText": "hello world",
                    "vectorStatus": "COMPLETED",
                    "createdAt": 1704067200000,
                    "updatedAt": 1704067200000,
                    "createdById": "user-1",
                    "updatedById": "user-1",
                },
                "memoryIndex": 0,
                "relevanceScore": 0.95,
            }
        }
        item = RetrievedItem.model_validate(raw)
        assert item.chunk is not None
        assert item.chunk.chunk.chunk_id == "c-1"
        assert item.memory is None


class TestRetrieveMemoryEvent:
    """Test RetrieveMemoryEvent with nested oneOf fields."""

    def test_event_with_retrieved_item(self):
        """End-to-end: model_validate succeeds and nested fields are typed."""
        from goodmem.models.retrieve_memory_event import RetrieveMemoryEvent

        raw = {
            "retrievedItem": {
                "memory": {
                    "memoryId": "mem-1",
                    "spaceId": "sp-1",
                    "contentType": "text/plain",
                    "processingStatus": "COMPLETED",
                    "pageImageStatus": "NONE",
                    "pageImageCount": 0,
                    "createdAt": 1704067200000,
                    "updatedAt": 1704067200000,
                    "createdById": "user-1",
                    "updatedById": "user-1",
                }
            }
        }
        event = RetrieveMemoryEvent.model_validate(raw)
        assert event.retrieved_item is not None
        assert event.retrieved_item.memory is not None
        assert event.retrieved_item.memory.memory_id == "mem-1"

    def test_null_fields_unchanged(self):
        """Null oneOf fields pass through without error."""
        from goodmem.models.retrieve_memory_event import RetrieveMemoryEvent

        raw = {"retrievedItem": None, "status": None}
        event = RetrieveMemoryEvent.model_validate(raw)
        assert event.retrieved_item is None

    def test_non_oneof_fields(self):
        """Fields that are not oneOf are properly typed."""
        from goodmem.models.retrieve_memory_event import RetrieveMemoryEvent

        raw = {
            "status": {"code": "PARTIAL_RESULTS", "message": "OK"},
        }
        event = RetrieveMemoryEvent.model_validate(raw)
        assert event.status is not None
        assert event.status.code == "PARTIAL_RESULTS"

    def test_streaming_event_parse(self, mock_client_factory):
        """Simulates parsing an NDJSON line from a retrieve stream."""
        from goodmem.models.retrieve_memory_event import RetrieveMemoryEvent

        ndjson_line = json.dumps({
            "retrievedItem": {
                "memory": {
                    "memoryId": "mem-42",
                    "spaceId": "sp-1",
                    "contentType": "text/plain",
                    "processingStatus": "COMPLETED",
                    "pageImageStatus": "NONE",
                    "pageImageCount": 0,
                    "createdAt": 1704067200000,
                    "updatedAt": 1704067200000,
                    "createdById": "user-1",
                    "updatedById": "user-1",
                }
            }
        })

        raw = json.loads(ndjson_line)
        event = RetrieveMemoryEvent.model_validate(raw)
        assert event.retrieved_item.memory.memory_id == "mem-42"


class TestContextItem:
    """Test ContextItem oneOf model."""

    def test_text_variant(self):
        from goodmem.models.context_item import ContextItem

        item = ContextItem.model_validate({"text": "Please focus on implementation details."})
        assert item.text == "Please focus on implementation details."
        assert item.binary is None

    def test_binary_variant(self):
        from goodmem.models.context_item import ContextItem

        item = ContextItem.model_validate({
            "binary": {"contentType": "image/png", "data": "iVBORw0KGg=="}
        })
        assert item.binary is not None
        assert item.binary.content_type == "image/png"
        assert item.text is None

    def test_rejects_both_variants(self):
        from pydantic import ValidationError
        from goodmem.models.context_item import ContextItem

        with pytest.raises(ValidationError, match="Exactly one of"):
            ContextItem.model_validate({
                "text": "hello",
                "binary": {"contentType": "image/png", "data": "iVBORw0KGg=="},
            })

    def test_rejects_zero_variants(self):
        from pydantic import ValidationError
        from goodmem.models.context_item import ContextItem

        with pytest.raises(ValidationError, match="Exactly one of"):
            ContextItem.model_validate({})


# ---------------------------------------------------------------------------
# Regression: oneOf and mutual exclusion validation (UNION-01/02/05/06)
# ---------------------------------------------------------------------------

class TestChunkingConfigurationValidation:
    """ChunkingConfiguration must enforce exactly one variant is set."""

    def test_rejects_empty(self):
        from goodmem.models.chunking_configuration import ChunkingConfiguration
        with pytest.raises(ValidationError, match="Exactly one of"):
            ChunkingConfiguration()

    def test_rejects_multiple(self):
        from goodmem.models.chunking_configuration import ChunkingConfiguration
        with pytest.raises(ValidationError, match="Exactly one of"):
            ChunkingConfiguration.model_validate({
                "none": {},
                "sentence": {
                    "maxChunkSize": 512,
                    "minChunkSize": 100,
                    "lengthMeasurement": "TOKEN_COUNT",
                },
            })

    def test_accepts_single(self):
        from goodmem.models.chunking_configuration import ChunkingConfiguration
        from goodmem.models.no_chunking_configuration import NoChunkingConfiguration
        config = ChunkingConfiguration(none=NoChunkingConfiguration())
        assert config.none is not None
        assert config.recursive is None


class TestEndpointAuthenticationKindPayload:
    """EndpointAuthentication must enforce kind ↔ payload consistency (UNION-02)."""

    def test_api_key_kind_requires_api_key_payload(self):
        from goodmem.models.endpoint_authentication import EndpointAuthentication
        with pytest.raises(ValidationError, match="requires 'api_key' to be set"):
            EndpointAuthentication(kind="CREDENTIAL_KIND_API_KEY")

    def test_api_key_kind_with_gcp_payload_rejected(self):
        from goodmem.models.endpoint_authentication import EndpointAuthentication
        from goodmem.models.gcp_adc_auth import GcpAdcAuth
        with pytest.raises(ValidationError):
            EndpointAuthentication(
                kind="CREDENTIAL_KIND_API_KEY",
                gcp_adc=GcpAdcAuth(),
            )

    def test_gcp_adc_kind_requires_gcp_payload(self):
        from goodmem.models.endpoint_authentication import EndpointAuthentication
        with pytest.raises(ValidationError, match="requires 'gcp_adc' to be set"):
            EndpointAuthentication(kind="CREDENTIAL_KIND_GCP_ADC")

    def test_unspecified_kind_rejects_payload(self):
        from goodmem.models.endpoint_authentication import EndpointAuthentication
        from goodmem.models.api_key_auth import ApiKeyAuth
        with pytest.raises(ValidationError, match="CREDENTIAL_KIND_UNSPECIFIED"):
            EndpointAuthentication(
                kind="CREDENTIAL_KIND_UNSPECIFIED",
                api_key=ApiKeyAuth(inline_secret="sk-test"),
            )

    def test_valid_gcp_adc_accepted(self):
        from goodmem.models.endpoint_authentication import EndpointAuthentication
        from goodmem.models.gcp_adc_auth import GcpAdcAuth
        ea = EndpointAuthentication(
            kind="CREDENTIAL_KIND_GCP_ADC",
            gcp_adc=GcpAdcAuth(),
        )
        assert ea.gcp_adc is not None
        assert ea.api_key is None


class TestEndpointAuthenticationMutualExclusion:
    """EndpointAuthentication must not allow both api_key and gcp_adc."""

    def test_rejects_both_payloads(self):
        from goodmem.models.endpoint_authentication import EndpointAuthentication
        from goodmem.models.credential_kind import CredentialKind
        from goodmem.models.api_key_auth import ApiKeyAuth
        from goodmem.models.gcp_adc_auth import GcpAdcAuth
        with pytest.raises(ValidationError, match="credential payload|mutually exclusive"):
            EndpointAuthentication(
                kind=CredentialKind.CREDENTIAL_KIND_API_KEY,
                api_key=ApiKeyAuth.model_validate({"inlineSecret": "sk-test"}),
                gcp_adc=GcpAdcAuth(),
            )

    def test_accepts_single_payload(self):
        from goodmem.models.endpoint_authentication import EndpointAuthentication
        from goodmem.models.credential_kind import CredentialKind
        from goodmem.models.api_key_auth import ApiKeyAuth
        auth = EndpointAuthentication(
            kind=CredentialKind.CREDENTIAL_KIND_API_KEY,
            api_key=ApiKeyAuth.model_validate({"inlineSecret": "sk-test"}),
        )
        assert auth.api_key is not None
        assert auth.gcp_adc is None


class TestUpdateSpaceRequestMutualExclusion:
    """UpdateSpaceRequest must not allow both replace_labels and merge_labels."""

    def test_rejects_both_label_strategies(self):
        from goodmem.models.update_space_request import UpdateSpaceRequest
        with pytest.raises(ValidationError, match="replace_labels.*merge_labels|mutually exclusive"):
            UpdateSpaceRequest(replace_labels={"a": "1"}, merge_labels={"b": "2"})

    def test_accepts_replace_only(self):
        from goodmem.models.update_space_request import UpdateSpaceRequest
        req = UpdateSpaceRequest(replace_labels={"a": "1"})
        assert req.replace_labels == {"a": "1"}
        assert req.merge_labels is None

    def test_accepts_neither(self):
        from goodmem.models.update_space_request import UpdateSpaceRequest
        req = UpdateSpaceRequest(name="Updated")
        assert req.replace_labels is None
        assert req.merge_labels is None


class TestOcrPageResultMutualExclusion:
    """OcrPageResult must not allow both page and status."""

    def test_rejects_both(self):
        from goodmem.models.ocr_page_result import OcrPageResult
        from goodmem.models.ocr_page import OcrPage
        from goodmem.models.rpc_status import RpcStatus
        page_obj = OcrPage.model_construct(markdown="# Title")
        status_obj = RpcStatus.model_construct(code=1, message="err")
        with pytest.raises(ValidationError, match="page.*status|mutually exclusive"):
            OcrPageResult(page_index=0, page=page_obj, status=status_obj)

    def test_accepts_page_only(self):
        from goodmem.models.ocr_page_result import OcrPageResult
        from goodmem.models.ocr_page import OcrPage
        page_obj = OcrPage.model_construct(markdown="# Title")
        result = OcrPageResult(page_index=0, page=page_obj)
        assert result.page is not None
        assert result.status is None


class TestMemoryFieldRequiredness:
    """Memory.page_image_status and page_image_count must be non-optional (TYPE-04)."""

    def test_page_image_status_is_required(self):
        from goodmem.models.memory import Memory
        field = Memory.model_fields["page_image_status"]
        assert field.is_required(), (
            "Memory.page_image_status must be required — TYPE-04"
        )

    def test_page_image_count_is_required(self):
        from goodmem.models.memory import Memory
        field = Memory.model_fields["page_image_count"]
        assert field.is_required(), (
            "Memory.page_image_count must be required — TYPE-04"
        )

    def test_memory_missing_page_image_status_rejected(self):
        from goodmem.models.memory import Memory
        with pytest.raises(ValidationError, match="pageImageStatus"):
            Memory.model_validate({
                "memoryId": "m-1", "spaceId": "s-1",
                "contentType": "text/plain", "processingStatus": "COMPLETED",
                "pageImageCount": 0,
                "createdAt": 1704067200000, "updatedAt": 1704067200000,
                "createdById": "u-1", "updatedById": "u-1",
            })

    def test_memory_with_both_fields_accepted(self):
        from goodmem.models.memory import Memory
        m = Memory.model_validate({
            "memoryId": "m-1", "spaceId": "s-1",
            "contentType": "text/plain", "processingStatus": "COMPLETED",
            "pageImageStatus": "NOT_APPLICABLE", "pageImageCount": 0,
            "createdAt": 1704067200000, "updatedAt": 1704067200000,
            "createdById": "u-1", "updatedById": "u-1",
        })
        assert m.page_image_status == "NOT_APPLICABLE"
        assert m.page_image_count == 0
