"""Tests for request deduplication and smart caching."""

from __future__ import annotations

import threading
import time

from orxaq.orchestration.deduplication import (
    CacheConfig,
    DeduplicationStats,
    RequestDeduplicator,
    _exact_hash,
    _normalize_prompt,
    _semantic_hash,
)


class TestNormalizePrompt:
    def test_basic(self):
        assert _normalize_prompt("Hello World") == "hello world"

    def test_extra_whitespace(self):
        assert _normalize_prompt("  hello   world  ") == "hello world"

    def test_tabs_newlines(self):
        assert _normalize_prompt("hello\n\tworld") == "hello world"

    def test_mixed_case(self):
        assert _normalize_prompt("HeLLo WoRLd") == "hello world"


class TestSemanticHash:
    def test_identical(self):
        h1 = _semantic_hash("test prompt")
        h2 = _semantic_hash("test prompt")
        assert h1 == h2

    def test_case_insensitive(self):
        h1 = _semantic_hash("Test Prompt")
        h2 = _semantic_hash("test prompt")
        assert h1 == h2

    def test_whitespace_insensitive(self):
        h1 = _semantic_hash("test  prompt")
        h2 = _semantic_hash("test prompt")
        assert h1 == h2

    def test_different_prompts(self):
        h1 = _semantic_hash("prompt A")
        h2 = _semantic_hash("prompt B")
        assert h1 != h2

    def test_model_matters(self):
        h1 = _semantic_hash("test", model="gpt-4")
        h2 = _semantic_hash("test", model="claude")
        assert h1 != h2


class TestExactHash:
    def test_identical(self):
        h1 = _exact_hash("test", "model", 1024, 0.7)
        h2 = _exact_hash("test", "model", 1024, 0.7)
        assert h1 == h2

    def test_different_params(self):
        h1 = _exact_hash("test", "model", 1024, 0.7)
        h2 = _exact_hash("test", "model", 2048, 0.7)
        assert h1 != h2


class TestDeduplicationStats:
    def test_defaults(self):
        s = DeduplicationStats()
        assert s.hit_rate == 0.0
        assert s.savings_pct == 0.0

    def test_hit_rate(self):
        s = DeduplicationStats(
            exact_hits=3,
            semantic_hits=2,
            misses=5,
            total_requests=10,
        )
        assert s.hit_rate == 0.5

    def test_savings_pct(self):
        s = DeduplicationStats(
            exact_hits=2,
            semantic_hits=1,
            coalesced=1,
            total_requests=10,
            misses=6,
        )
        assert s.savings_pct == 40.0

    def test_to_dict(self):
        s = DeduplicationStats(total_requests=5, exact_hits=2)
        d = s.to_dict()
        assert d["total_requests"] == 5
        assert d["exact_hits"] == 2


class TestCacheConfig:
    def test_defaults(self):
        c = CacheConfig()
        assert c.exact_maxsize == 512
        assert c.enable_semantic is True

    def test_to_dict(self):
        c = CacheConfig(exact_maxsize=100)
        d = c.to_dict()
        assert d["exact_maxsize"] == 100


class TestDeduplicatorExactCache:
    def test_miss_returns_none(self):
        d = RequestDeduplicator()
        assert d.get("test") is None

    def test_exact_hit(self):
        d = RequestDeduplicator()
        d.put("test", "model", 1024, 0.7, "response-1")
        result = d.get("test", "model", 1024, 0.7)
        assert result == "response-1"

    def test_different_params_exact_miss(self):
        cfg = CacheConfig(enable_semantic=False)
        d = RequestDeduplicator(config=cfg)
        d.put("test", "model", 1024, 0.7, "response-1")
        assert d.get("test", "model", 2048, 0.7) is None

    def test_different_params_semantic_hit(self):
        d = RequestDeduplicator()
        d.put("test", "model", 1024, 0.7, "response-1")
        result = d.get("test", "model", 2048, 0.7)
        assert result == "response-1"

    def test_stats_exact_hit(self):
        d = RequestDeduplicator()
        d.put("test", "model", 1024, 0.7, "response-1")
        d.get("test", "model", 1024, 0.7)
        s = d.stats
        assert s.exact_hits == 1
        assert s.total_requests == 1


class TestDeduplicatorSemanticCache:
    def test_semantic_match_case(self):
        d = RequestDeduplicator()
        d.put("Hello World", "model", 1024, 0.7, "response-1")
        result = d.get("hello world", "model", 1024, 0.7)
        assert result == "response-1"

    def test_semantic_match_whitespace(self):
        d = RequestDeduplicator()
        d.put("test  prompt", "model", 1024, 0.7, "response-1")
        result = d.get("test prompt", "model", 1024, 0.7)
        assert result == "response-1"

    def test_semantic_disabled(self):
        cfg = CacheConfig(enable_semantic=False)
        d = RequestDeduplicator(config=cfg)
        d.put("Hello World", "model", 1024, 0.7, "response-1")
        result = d.get("hello world", "model", 1024, 0.7)
        assert result is None

    def test_stats_semantic_hit(self):
        d = RequestDeduplicator()
        d.put("Hello World", "model", 1024, 0.7, "resp")
        d.get("hello world", "model", 2048, 0.7)
        s = d.stats
        assert s.semantic_hits == 1


class TestDeduplicatorCoalescing:
    def test_coalesce_no_inflight(self):
        d = RequestDeduplicator()
        coalesced, result = d.try_coalesce("test")
        assert coalesced is False
        assert result is None

    def test_coalesce_with_inflight(self):
        d = RequestDeduplicator()
        results: list = []
        errors: list = []

        coalesced, _ = d.try_coalesce("test", "model", 1024, 0.7)
        assert coalesced is False

        def waiter():
            try:
                c, r = d.try_coalesce("test", "model", 1024, 0.7, timeout_s=2.0)
                results.append((c, r))
            except Exception as e:
                errors.append(str(e))

        t = threading.Thread(target=waiter)
        t.start()
        time.sleep(0.05)
        d.complete_inflight("test", "model", 1024, 0.7, "shared-response")
        t.join(timeout=3.0)

        assert len(errors) == 0
        assert len(results) == 1
        assert results[0] == (True, "shared-response")

    def test_coalesce_disabled(self):
        cfg = CacheConfig(enable_coalescing=False)
        d = RequestDeduplicator(config=cfg)
        coalesced, result = d.try_coalesce("test")
        assert coalesced is False


class TestDeduplicatorClear:
    def test_clear(self):
        d = RequestDeduplicator()
        d.put("test", "model", 1024, 0.7, "response")
        d.get("test", "model", 1024, 0.7)
        d.clear()
        s = d.stats
        assert s.total_requests == 0
        assert s.exact_hits == 0
        assert d.get("test", "model", 1024, 0.7) is None


class TestDeduplicatorSummary:
    def test_summary(self):
        d = RequestDeduplicator()
        d.put("test", "model", 1024, 0.7, "response")
        s = d.summary()
        assert "config" in s
        assert "stats" in s
        assert "exact_cache" in s
        assert "semantic_cache" in s
        assert s["exact_cache"]["entries"] == 1


class TestDeduplicatorThreadSafety:
    def test_concurrent_puts_and_gets(self):
        d = RequestDeduplicator()
        errors: list[str] = []

        def worker(i: int):
            try:
                prompt = f"prompt-{i % 5}"
                d.put(prompt, "model", 1024, 0.7, f"response-{i}")
                d.get(prompt, "model", 1024, 0.7)
            except Exception as e:
                errors.append(str(e))

        threads = [threading.Thread(target=worker, args=(i,)) for i in range(20)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0
        s = d.stats
        assert s.total_requests == 20


# ── Targeted coverage (line 220: coalesce timeout returns False) ──


class TestCoalesceTimeout:
    """Cover line 220: try_coalesce timeout returns (False, None)."""

    def test_coalesce_timeout(self):
        d = RequestDeduplicator()
        # Register an inflight request but never complete it
        d.try_coalesce("prompt-x", "model", 1024, 0.7)
        # Second call waits for the inflight event but times out
        ok, result = d.try_coalesce("prompt-x", "model", 1024, 0.7, timeout_s=0.001)
        assert ok is False
        assert result is None
