import hashlib
import time
import pytest
from unittest.mock import patch, MagicMock
import transduck
from transduck import ait, ait_plural, set_language, on, off
from transduck.hooks import TranslationEvent
from transduck.config import TransduckConfig


def _hash(text: str) -> str:
    return hashlib.sha256(text.encode()).hexdigest()


@pytest.fixture
def mock_config(tmp_path):
    return TransduckConfig(
        project_name="test",
        project_context="A test site",
        source_lang="EN",
        target_langs=["DE", "ES"],
        storage_path=tmp_path / "test.db",
        provider="openai",
        api_key_env="OPENAI_API_KEY",
        token_env="",
        backend_model="gpt-4.1-mini",
        backend_timeout=10,
        backend_max_retries=2,
        shared_url=None,
        read_only=False,
    )


@pytest.fixture(autouse=True)
def reset_state():
    """Reset global state between tests."""
    transduck._state = transduck._State()
    yield
    transduck._wait_for_background(timeout=2.0)
    transduck._state = transduck._State()


@pytest.fixture
def initialized(mock_config, monkeypatch):
    monkeypatch.setenv("OPENAI_API_KEY", "test-key")
    transduck.initialize(mock_config)
    set_language("DE")


def _pre_insert(source_text="Hello", translated_text="Hallo", context=""):
    """Pre-insert a translation into the local cache."""
    transduck._state.store.insert(
        source_text=source_text,
        source_lang="EN",
        target_lang="DE",
        project_context_hash=_hash("A test site"),
        string_context_hash=_hash(context),
        translated_text=translated_text,
        model="gpt-4.1-mini",
        string_context=context,
    )


# --- Global hook tests ---

def test_global_hook_fires_on_cache_hit(initialized):
    _pre_insert()
    events = []
    on("translate", lambda e: events.append(e))
    ait("Hello")
    assert len(events) == 1
    assert events[0].source == "cache"
    assert events[0].translated_text == "Hallo"
    assert events[0].source_text == "Hello"
    assert events[0].source_lang == "EN"
    assert events[0].lang == "DE"
    assert events[0].project_context == "A test site"
    assert events[0].plural is False


@patch("transduck.backend.translate")
def test_global_hook_fires_on_fresh_translation(mock_translate, initialized):
    mock_translate.return_value = "Hallo"
    events = []
    on("translate", lambda e: events.append(e))
    ait("Hello")
    assert len(events) == 1
    assert events[0].source == "translated"
    assert events[0].translated_text == "Hallo"


@patch("transduck.backend.translate")
def test_global_hook_fires_with_context(mock_translate, initialized):
    mock_translate.return_value = "Buchen"
    events = []
    on("translate", lambda e: events.append(e))
    ait("Book", context="Button to book a trip")
    assert len(events) == 1
    assert events[0].context == "Button to book a trip"
    assert events[0].project_context == "A test site"


def test_off_removes_hook(initialized):
    _pre_insert()
    events = []
    hook = lambda e: events.append(e)
    on("translate", hook)
    ait("Hello")
    assert len(events) == 1
    off("translate", hook)
    ait("Hello")
    assert len(events) == 1  # no new event


def test_multiple_global_hooks_fire_in_order(initialized):
    _pre_insert()
    order = []
    on("translate", lambda e: order.append("first"))
    on("translate", lambda e: order.append("second"))
    ait("Hello")
    assert order == ["first", "second"]


# --- Per-call hook tests ---

def test_per_call_hook_fires(initialized):
    _pre_insert()
    events = []
    ait("Hello", on_translate=lambda e: events.append(e))
    assert len(events) == 1
    assert events[0].translated_text == "Hallo"


@patch("transduck.backend.translate")
def test_per_call_hook_on_fresh_translation(mock_translate, initialized):
    mock_translate.return_value = "Hallo"
    events = []
    ait("Hello", on_translate=lambda e: events.append(e))
    assert len(events) == 1
    assert events[0].source == "translated"


# --- Global + per-call combination ---

def test_global_fires_before_per_call(initialized):
    _pre_insert()
    order = []
    on("translate", lambda e: order.append("global"))
    ait("Hello", on_translate=lambda e: order.append("per_call"))
    assert order == ["global", "per_call"]


# --- Error isolation ---

def test_hook_error_does_not_crash_ait(initialized):
    _pre_insert()

    def bad_hook(e):
        raise ValueError("hook exploded")

    on("translate", bad_hook)
    result = ait("Hello")
    assert result == "Hallo"  # translation still works


def test_per_call_hook_error_does_not_crash_ait(initialized):
    _pre_insert()
    result = ait("Hello", on_translate=lambda e: 1 / 0)
    assert result == "Hallo"


# --- No hooks = no overhead ---

@patch("transduck.backend.translate")
def test_no_hooks_still_works(mock_translate, initialized):
    mock_translate.return_value = "Hallo"
    result = ait("Hello")
    assert result == "Hallo"


# --- TranslationResult.source property ---

def test_result_source_on_cache_hit(initialized):
    _pre_insert()
    result = ait("Hello")
    assert result.source == "cache"


@patch("transduck.backend.translate")
def test_result_source_on_fresh_translation(mock_translate, initialized):
    mock_translate.return_value = "Hallo"
    result = ait("Hello")
    assert result.source == "translated"


# --- Plural hook tests ---

@patch("transduck.backend.translate_plural")
def test_plural_hook_fires_on_fresh_translation(mock_translate_plural, initialized):
    mock_translate_plural.return_value = {"one": "{count} Nacht", "other": "{count} Nächte"}
    events = []
    on("translate", lambda e: events.append(e))
    ait_plural("{count} night", "{count} nights", count=1)
    assert len(events) == 1
    assert events[0].plural is True
    assert events[0].plural_category == "one"
    assert events[0].plural_forms == {"one": "{count} Nacht", "other": "{count} Nächte"}
    assert events[0].source == "translated"


@patch("transduck.backend.translate_plural")
def test_plural_per_call_hook(mock_translate_plural, initialized):
    mock_translate_plural.return_value = {"one": "{count} Nacht", "other": "{count} Nächte"}
    events = []
    ait_plural("{count} night", "{count} nights", count=7,
               on_translate=lambda e: events.append(e))
    assert len(events) == 1
    assert events[0].plural is True
    assert events[0].plural_category == "other"


def test_plural_hook_on_cache_hit(initialized):
    source_key = "{count} night\x00{count} nights"
    transduck._state.store.insert_plural(
        source_text=source_key, source_lang="EN", target_lang="DE",
        project_context_hash=_hash("A test site"), string_context_hash=_hash(""),
        plural_category="one", translated_text="{count} Nacht",
        model="gpt-4.1-mini", string_context="",
    )
    transduck._state.store.insert_plural(
        source_text=source_key, source_lang="EN", target_lang="DE",
        project_context_hash=_hash("A test site"), string_context_hash=_hash(""),
        plural_category="other", translated_text="{count} Nächte",
        model="gpt-4.1-mini", string_context="",
    )
    events = []
    on("translate", lambda e: events.append(e))
    ait_plural("{count} night", "{count} nights", count=1)
    assert len(events) == 1
    assert events[0].source == "cache"
    assert events[0].plural_category == "one"


# --- Background mode ---

@patch("transduck.backend.translate")
def test_background_hook_fires_after_translation(mock_translate, initialized):
    mock_translate.return_value = "Hallo"
    events = []
    ait("Hello", background=True, on_translate=lambda e: events.append(e))
    # Wait for background thread
    time.sleep(0.5)
    assert len(events) == 1
    assert events[0].source == "translated"
