"""
Unit tests for Context Merger.
[AC-AISVC-14, AC-AISVC-15] Tests for context merging and truncation.

Tests cover:
- Message fingerprint computation
- Context merging with deduplication
- Token-based truncation
- Complete merge_and_truncate pipeline
"""

import hashlib
from unittest.mock import MagicMock, patch

import pytest

from app.models import ChatMessage, Role
from app.services.context import ContextMerger, MergedContext, get_context_merger


@pytest.fixture
def mock_settings():
    """Mock settings for testing."""
    settings = MagicMock()
    return settings


@pytest.fixture
def context_merger(mock_settings):
    """Create context merger with mocked settings."""
    with patch("app.services.context.get_settings", return_value=mock_settings):
        merger = ContextMerger(max_history_tokens=1000)
        yield merger


@pytest.fixture
def local_history():
    """Sample local history messages."""
    return [
        ChatMessage(role=Role.USER, content="Hello"),
        ChatMessage(role=Role.ASSISTANT, content="Hi there!"),
        ChatMessage(role=Role.USER, content="How are you?"),
    ]


@pytest.fixture
def external_history():
    """Sample external history messages."""
    return [
        ChatMessage(role=Role.USER, content="Hello"),
        ChatMessage(role=Role.ASSISTANT, content="Hi there!"),
        ChatMessage(role=Role.USER, content="What's the weather?"),
    ]


@pytest.fixture
def dict_local_history():
    """Sample local history as dicts."""
    return [
        {"role": "user", "content": "Hello"},
        {"role": "assistant", "content": "Hi there!"},
    ]


@pytest.fixture
def dict_external_history():
    """Sample external history as dicts."""
    return [
        {"role": "user", "content": "Hello"},
        {"role": "user", "content": "What's the weather?"},
    ]


class TestFingerprintComputation:
    """Tests for message fingerprint computation. [AC-AISVC-15]"""

    def test_fingerprint_consistency(self, context_merger):
        """Test that same input produces same fingerprint."""
        fp1 = context_merger.compute_fingerprint("user", "Hello world")
        fp2 = context_merger.compute_fingerprint("user", "Hello world")
        assert fp1 == fp2

    def test_fingerprint_role_difference(self, context_merger):
        """Test that different roles produce different fingerprints."""
        fp_user = context_merger.compute_fingerprint("user", "Hello")
        fp_assistant = context_merger.compute_fingerprint("assistant", "Hello")
        assert fp_user != fp_assistant

    def test_fingerprint_content_difference(self, context_merger):
        """Test that different content produces different fingerprints."""
        fp1 = context_merger.compute_fingerprint("user", "Hello")
        fp2 = context_merger.compute_fingerprint("user", "World")
        assert fp1 != fp2

    def test_fingerprint_normalization(self, context_merger):
        """Test that content is normalized (trimmed)."""
        fp1 = context_merger.compute_fingerprint("user", "Hello")
        fp2 = context_merger.compute_fingerprint("user", "  Hello  ")
        assert fp1 == fp2

    def test_fingerprint_is_sha256(self, context_merger):
        """Test that fingerprint is SHA256 hash."""
        fp = context_merger.compute_fingerprint("user", "Hello")
        expected = hashlib.sha256("user|Hello".encode("utf-8")).hexdigest()
        assert fp == expected
        assert len(fp) == 64  # SHA256 produces 64 hex characters


class TestContextMerging:
    """Tests for context merging with deduplication. [AC-AISVC-14, AC-AISVC-15]"""

    def test_merge_empty_histories(self, context_merger):
        """[AC-AISVC-14] Test merging empty histories."""
        result = context_merger.merge_context(None, None)

        assert isinstance(result, MergedContext)
        assert result.messages == []
        assert result.local_count == 0
        assert result.external_count == 0
        assert result.duplicates_skipped == 0

    def test_merge_local_only(self, context_merger, local_history):
        """[AC-AISVC-14] Test merging with only local history (no external)."""
        result = context_merger.merge_context(local_history, None)

        assert len(result.messages) == 3
        assert result.local_count == 3
        assert result.external_count == 0
        assert result.duplicates_skipped == 0

    def test_merge_external_only(self, context_merger, external_history):
        """[AC-AISVC-15] Test merging with only external history (no local)."""
        result = context_merger.merge_context(None, external_history)

        assert len(result.messages) == 3
        assert result.local_count == 0
        assert result.external_count == 3
        assert result.duplicates_skipped == 0

    def test_merge_with_duplicates(self, context_merger, local_history, external_history):
        """[AC-AISVC-15] Test deduplication when merging overlapping histories."""
        result = context_merger.merge_context(local_history, external_history)

        assert len(result.messages) == 4
        assert result.local_count == 3
        assert result.external_count == 1
        assert result.duplicates_skipped == 2

        roles = [m["role"] for m in result.messages]
        contents = [m["content"] for m in result.messages]
        assert "What's the weather?" in contents

    def test_merge_with_dict_histories(self, context_merger, dict_local_history, dict_external_history):
        """[AC-AISVC-14, AC-AISVC-15] Test merging with dict format histories."""
        result = context_merger.merge_context(dict_local_history, dict_external_history)

        assert len(result.messages) == 3
        assert result.local_count == 2
        assert result.external_count == 1
        assert result.duplicates_skipped == 1

    def test_merge_priority_local(self, context_merger):
        """[AC-AISVC-15] Test that local history takes priority."""
        local = [ChatMessage(role=Role.USER, content="Hello")]
        external = [ChatMessage(role=Role.USER, content="Hello")]

        result = context_merger.merge_context(local, external)

        assert len(result.messages) == 1
        assert result.duplicates_skipped == 1

    def test_merge_records_diagnostics(self, context_merger, local_history, external_history):
        """[AC-AISVC-15] Test that duplicates are recorded in diagnostics."""
        result = context_merger.merge_context(local_history, external_history)

        assert len(result.diagnostics) == 2
        for diag in result.diagnostics:
            assert diag["type"] == "duplicate_skipped"
            assert "role" in diag
            assert "content_preview" in diag


class TestTokenTruncation:
    """Tests for token-based truncation. [AC-AISVC-14]"""

    def test_truncate_empty_messages(self, context_merger):
        """[AC-AISVC-14] Test truncating empty message list."""
        truncated, count = context_merger.truncate_context([], 100)
        assert truncated == []
        assert count == 0

    def test_truncate_within_budget(self, context_merger):
        """[AC-AISVC-14] Test that messages within budget are not truncated."""
        messages = [
            {"role": "user", "content": "Hello"},
            {"role": "assistant", "content": "Hi"},
        ]
        truncated, count = context_merger.truncate_context(messages, 1000)

        assert len(truncated) == 2
        assert count == 0

    def test_truncate_exceeds_budget(self, context_merger):
        """[AC-AISVC-14] Test that messages exceeding budget are truncated."""
        messages = [
            {"role": "user", "content": "Hello world " * 100},
            {"role": "assistant", "content": "Hi there " * 100},
            {"role": "user", "content": "Short message"},
        ]
        truncated, count = context_merger.truncate_context(messages, 50)

        assert len(truncated) < len(messages)
        assert count > 0

    def test_truncate_keeps_recent_messages(self, context_merger):
        """[AC-AISVC-14] Test that truncation keeps most recent messages."""
        messages = [
            {"role": "user", "content": "First message"},
            {"role": "assistant", "content": "Second message"},
            {"role": "user", "content": "Third message"},
        ]
        truncated, count = context_merger.truncate_context(messages, 20)

        if count > 0:
            assert "Third message" in [m["content"] for m in truncated]

    def test_truncate_with_default_budget(self, context_merger):
        """[AC-AISVC-14] Test truncation with default budget from config."""
        messages = [{"role": "user", "content": "Test"}]
        truncated, count = context_merger.truncate_context(messages)

        assert len(truncated) == 1
        assert count == 0


class TestMergeAndTruncate:
    """Tests for complete merge_and_truncate pipeline. [AC-AISVC-14, AC-AISVC-15]"""

    def test_merge_and_truncate_combined(self, context_merger):
        """[AC-AISVC-14, AC-AISVC-15] Test complete pipeline."""
        local = [
            ChatMessage(role=Role.USER, content="Hello"),
            ChatMessage(role=Role.ASSISTANT, content="Hi"),
        ]
        external = [
            ChatMessage(role=Role.USER, content="Hello"),
            ChatMessage(role=Role.USER, content="What's up?"),
        ]

        result = context_merger.merge_and_truncate(local, external, max_tokens=1000)

        assert isinstance(result, MergedContext)
        assert len(result.messages) == 3
        assert result.local_count == 2
        assert result.external_count == 1
        assert result.duplicates_skipped == 1

    def test_merge_and_truncate_with_truncation(self, context_merger):
        """[AC-AISVC-14, AC-AISVC-15] Test pipeline with truncation."""
        local = [
            ChatMessage(role=Role.USER, content="Hello " * 50),
            ChatMessage(role=Role.ASSISTANT, content="Hi " * 50),
        ]
        external = [
            ChatMessage(role=Role.USER, content="Short"),
        ]

        result = context_merger.merge_and_truncate(local, external, max_tokens=50)

        assert result.truncated_count > 0
        assert result.total_tokens <= 50


class TestContextMergerSingleton:
    """Tests for singleton pattern."""

    def test_get_context_merger_singleton(self, mock_settings):
        """Test that get_context_merger returns singleton."""
        with patch("app.services.context.get_settings", return_value=mock_settings):
            from app.services.context import _context_merger
            import app.services.context as context_module
            context_module._context_merger = None

            merger1 = get_context_merger()
            merger2 = get_context_merger()

            assert merger1 is merger2
