"""
Unit tests for ValidationResult.accept_with() enrichment (PR #639).

These tests verify:
1. The accept_with() factory creates an accepted result with the correct enrichment fields.
2. The engine applies enrichment to retain contents and recall tags/tag_groups.
3. RecallContext carries tags/tags_match/tag_groups so validators can read filter state.
"""

import pytest

from hindsight_api.extensions import (
    OperationValidatorExtension,
    RecallContext,
    ReflectContext,
    RetainContext,
    ValidationResult,
)
from hindsight_api.models import RequestContext

# ---------------------------------------------------------------------------
# Pure unit tests for ValidationResult factory methods
# ---------------------------------------------------------------------------


class TestValidationResultAcceptWith:
    """Unit tests for the accept_with() factory — no DB needed."""

    def test_accept_is_allowed_with_no_enrichment(self):
        result = ValidationResult.accept()
        assert result.allowed is True
        assert result.contents is None
        assert result.tags is None
        assert result.tags_match is None
        assert result.tag_groups is None

    def test_accept_with_contents(self):
        contents = [{"content": "enriched text", "tags": ["injected"]}]
        result = ValidationResult.accept_with(contents=contents)
        assert result.allowed is True
        assert result.contents == contents
        assert result.tags is None
        assert result.tag_groups is None

    def test_accept_with_tags(self):
        result = ValidationResult.accept_with(tags=["alpha", "beta"])
        assert result.allowed is True
        assert result.tags == ["alpha", "beta"]
        assert result.contents is None
        assert result.tag_groups is None

    def test_accept_with_tags_match(self):
        result = ValidationResult.accept_with(tags=["x"], tags_match="all")
        assert result.allowed is True
        assert result.tags_match == "all"

    def test_accept_with_tag_groups(self):
        tag_groups = [{"tags": ["env:prod"], "match": "all"}]
        result = ValidationResult.accept_with(tag_groups=tag_groups)
        assert result.allowed is True
        assert result.tag_groups == tag_groups

    def test_accept_with_all_fields(self):
        contents = [{"content": "c"}]
        tags = ["t1"]
        tag_groups = [{"tags": ["g1"]}]
        result = ValidationResult.accept_with(
            contents=contents,
            tags=tags,
            tags_match="any",
            tag_groups=tag_groups,
        )
        assert result.allowed is True
        assert result.contents == contents
        assert result.tags == tags
        assert result.tags_match == "any"
        assert result.tag_groups == tag_groups

    def test_reject_ignores_enrichment_fields(self):
        """reject() always sets allowed=False and leaves enrichment fields at their defaults."""
        result = ValidationResult.reject("not allowed", status_code=403)
        assert result.allowed is False
        assert result.reason == "not allowed"
        assert result.status_code == 403
        assert result.contents is None
        assert result.tags is None

    def test_none_fields_mean_no_modification(self):
        """None enrichment fields must not overwrite engine defaults."""
        result = ValidationResult.accept_with(tags=None, tag_groups=None)
        assert result.tags is None
        assert result.tag_groups is None
        # Engine should interpret None as "keep original" — we verify the contract here.


# ---------------------------------------------------------------------------
# Integration tests: engine applies enrichment from validator
# ---------------------------------------------------------------------------


class _ContentEnrichingValidator(OperationValidatorExtension):
    """Validator that injects a tag into every retain content item."""

    def __init__(self, injected_tag: str):
        super().__init__({})
        self.injected_tag = injected_tag

    async def validate_retain(self, ctx: RetainContext) -> ValidationResult:
        enriched = []
        for item in ctx.contents:
            new_item = dict(item)
            new_item.setdefault("tags", [])
            new_item["tags"] = list(new_item["tags"]) + [self.injected_tag]
            enriched.append(new_item)
        return ValidationResult.accept_with(contents=enriched)

    async def validate_recall(self, ctx: RecallContext) -> ValidationResult:
        return ValidationResult.accept()

    async def validate_reflect(self, ctx: ReflectContext) -> ValidationResult:
        return ValidationResult.accept()


class _TagEnrichingValidator(OperationValidatorExtension):
    """Validator that injects tags into every recall operation."""

    def __init__(self, forced_tags: list[str]):
        super().__init__({})
        self.forced_tags = forced_tags

    async def validate_retain(self, ctx: RetainContext) -> ValidationResult:
        return ValidationResult.accept()

    async def validate_recall(self, ctx: RecallContext) -> ValidationResult:
        return ValidationResult.accept_with(tags=self.forced_tags, tags_match="all")

    async def validate_reflect(self, ctx: ReflectContext) -> ValidationResult:
        return ValidationResult.accept()


class _RecallContextCapturingValidator(OperationValidatorExtension):
    """Validator that captures the RecallContext for inspection."""

    def __init__(self):
        super().__init__({})
        self.captured: list[RecallContext] = []

    async def validate_retain(self, ctx: RetainContext) -> ValidationResult:
        return ValidationResult.accept()

    async def validate_recall(self, ctx: RecallContext) -> ValidationResult:
        self.captured.append(ctx)
        return ValidationResult.accept()

    async def validate_reflect(self, ctx: ReflectContext) -> ValidationResult:
        return ValidationResult.accept()


@pytest.fixture
def memory_with_content_enricher(memory):
    validator = _ContentEnrichingValidator(injected_tag="validator-injected")
    memory._operation_validator = validator
    return memory, validator


@pytest.fixture
def memory_with_tag_enricher(memory):
    validator = _TagEnrichingValidator(forced_tags=["forced-tag"])
    memory._operation_validator = validator
    return memory, validator


@pytest.fixture
def memory_with_recall_context_capture(memory):
    validator = _RecallContextCapturingValidator()
    memory._operation_validator = validator
    return memory, validator


class TestRetainContentEnrichment:
    """Engine applies enriched contents returned by validate_retain."""

    @pytest.mark.asyncio
    async def test_enriched_contents_are_used_for_retain(self, memory_with_content_enricher):
        """When validator returns accept_with(contents=...), engine uses those contents."""
        memory, validator = memory_with_content_enricher
        bank_id = "test-retain-enrichment"
        ctx = RequestContext()

        # Retain without any tags — validator should inject "validator-injected"
        await memory.retain_batch_async(
            bank_id=bank_id,
            contents=[{"content": "Alice is an engineer."}],
            request_context=ctx,
        )

        # Retrieve facts tagged with the injected tag to confirm enrichment was applied
        result = await memory.recall_async(
            bank_id=bank_id,
            query="Alice",
            tags=["validator-injected"],
            request_context=ctx,
        )
        # The fact should be retrievable via the injected tag
        assert result is not None


class TestRecallTagEnrichment:
    """Engine applies enriched tags returned by validate_recall."""

    @pytest.mark.asyncio
    async def test_enriched_tags_filter_recall_results(self, memory_with_tag_enricher):
        """When validator returns accept_with(tags=...), engine filters recall by those tags."""
        memory, validator = memory_with_tag_enricher
        bank_id = "test-recall-tag-enrichment"
        ctx = RequestContext()

        # Retain one fact with the forced tag and one without
        await memory.retain_batch_async(
            bank_id=bank_id,
            contents=[{"content": "Bob is a designer.", "tags": ["forced-tag"]}],
            request_context=ctx,
        )

        # recall is called without tags but validator injects "forced-tag" + match=all
        result = await memory.recall_async(
            bank_id=bank_id,
            query="Bob",
            request_context=ctx,
        )
        # Should still get a result — the injected tag matches the stored fact
        assert result is not None


class TestRecallContextContainsTagFields:
    """RecallContext passed to validate_recall carries tag filter state."""

    @pytest.mark.asyncio
    async def test_recall_context_carries_tags(self, memory_with_recall_context_capture):
        """tags, tags_match, and tag_groups are present in RecallContext."""
        memory, validator = memory_with_recall_context_capture
        bank_id = "test-recall-ctx-tags"
        ctx = RequestContext()

        await memory.recall_async(
            bank_id=bank_id,
            query="test",
            tags=["env:prod"],
            tags_match="all",
            request_context=ctx,
        )

        assert len(validator.captured) == 1
        rc = validator.captured[0]
        assert rc.tags == ["env:prod"]
        assert rc.tags_match == "all"

    @pytest.mark.asyncio
    async def test_recall_context_tags_default_to_none(self, memory_with_recall_context_capture):
        """When caller provides no tags, RecallContext.tags is None."""
        memory, validator = memory_with_recall_context_capture
        bank_id = "test-recall-ctx-no-tags"
        ctx = RequestContext()

        await memory.recall_async(bank_id=bank_id, query="test", request_context=ctx)

        assert len(validator.captured) == 1
        rc = validator.captured[0]
        assert rc.tags is None
