import hashlib
from unittest.mock import MagicMock, patch, call

import pytest


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


def _content_hash(source_text, project_context_hash, string_context_hash):
    return hashlib.sha256(
        (source_text + "\x00" + project_context_hash + "\x00" + string_context_hash).encode()
    ).hexdigest()


@pytest.fixture
def mock_psycopg2():
    mock_conn = MagicMock()
    mock_conn.autocommit = False
    mock_cursor = MagicMock()
    mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor)
    mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
    with patch.dict("sys.modules", {"psycopg2": MagicMock()}):
        import sys
        sys.modules["psycopg2"].connect.return_value = mock_conn
        yield mock_conn, mock_cursor


@pytest.fixture
def store(mock_psycopg2):
    mock_conn, mock_cursor = mock_psycopg2
    from transduck.shared_store import SharedStore
    s = SharedStore("postgresql://localhost/test")
    return s, mock_conn, mock_cursor


def test_initialize_creates_table(store):
    s, mock_conn, mock_cursor = store
    s.initialize()
    mock_cursor.execute.assert_called_once()
    sql = mock_cursor.execute.call_args[0][0]
    assert "CREATE TABLE IF NOT EXISTS transduck_translations" in sql
    mock_conn.commit.assert_called()


def test_lookup_returns_none_on_miss(store):
    s, mock_conn, mock_cursor = store
    mock_cursor.fetchone.return_value = None
    result = s.lookup(
        source_text="Hello",
        source_lang="EN",
        target_lang="DE",
        project_context_hash=_hash("ctx"),
        string_context_hash=_hash(""),
    )
    assert result is None
    assert mock_cursor.execute.call_count == 2


def test_lookup_returns_text_on_hit(store):
    s, mock_conn, mock_cursor = store
    mock_cursor.fetchone.return_value = ("Hallo",)
    result = s.lookup(
        source_text="Hello",
        source_lang="EN",
        target_lang="DE",
        project_context_hash=_hash("ctx"),
        string_context_hash=_hash(""),
    )
    assert result == "Hallo"


def test_insert_executes_and_commits(store):
    s, mock_conn, mock_cursor = store
    s.insert(
        source_text="Hello",
        source_lang="EN",
        target_lang="DE",
        project_context_hash=_hash("ctx"),
        string_context_hash=_hash(""),
        translated_text="Hallo",
        model="gpt-4.1-mini",
    )
    mock_cursor.execute.assert_called_once()
    sql = mock_cursor.execute.call_args[0][0]
    assert "INSERT INTO transduck_translations" in sql
    assert "ON CONFLICT DO NOTHING" in sql
    mock_conn.commit.assert_called()


def test_insert_with_failed_status(store):
    s, mock_conn, mock_cursor = store
    s.insert(
        source_text="Hello",
        source_lang="EN",
        target_lang="DE",
        project_context_hash=_hash("ctx"),
        string_context_hash=_hash(""),
        translated_text="Hallo",
        model="gpt-4.1-mini",
        status="failed",
    )
    params = mock_cursor.execute.call_args[0][1]
    assert "failed" in params


def test_lookup_plural_returns_dict(store):
    s, mock_conn, mock_cursor = store
    mock_cursor.fetchall.return_value = [("one", "1 Nachricht"), ("other", "2 Nachrichten")]
    result = s.lookup_plural(
        source_text="key",
        source_lang="EN",
        target_lang="DE",
        project_context_hash=_hash("ctx"),
        string_context_hash=_hash(""),
    )
    assert result == {"one": "1 Nachricht", "other": "2 Nachrichten"}


def test_lookup_plural_returns_empty_on_miss(store):
    s, mock_conn, mock_cursor = store
    mock_cursor.fetchall.return_value = []
    result = s.lookup_plural(
        source_text="key",
        source_lang="EN",
        target_lang="DE",
        project_context_hash=_hash("ctx"),
        string_context_hash=_hash(""),
    )
    assert result == {}


def test_insert_plural_executes_and_commits(store):
    s, mock_conn, mock_cursor = store
    s.insert_plural(
        source_text="key",
        source_lang="EN",
        target_lang="DE",
        project_context_hash=_hash("ctx"),
        string_context_hash=_hash(""),
        plural_category="one",
        translated_text="1 Nachricht",
        model="gpt-4.1-mini",
    )
    mock_cursor.execute.assert_called_once()
    sql = mock_cursor.execute.call_args[0][0]
    assert "INSERT INTO transduck_translations" in sql
    mock_conn.commit.assert_called()


def test_close(store):
    s, mock_conn, mock_cursor = store
    s.close()
    mock_conn.close.assert_called_once()


def test_close_idempotent(store):
    s, mock_conn, mock_cursor = store
    s.close()
    s.close()  # should not raise


def test_content_hash_used_in_lookup(store):
    s, mock_conn, mock_cursor = store
    mock_cursor.fetchone.return_value = None
    pch = _hash("ctx")
    sch = _hash("")
    s.lookup(
        source_text="Hello",
        source_lang="EN",
        target_lang="DE",
        project_context_hash=pch,
        string_context_hash=sch,
    )
    # First call is the primary lookup; second is the context mismatch check
    params = mock_cursor.execute.call_args_list[0][0][1]
    expected_hash = _content_hash("Hello", pch, sch)
    assert expected_hash in params


def test_lookup_miss_logs_debug_on_context_mismatch(store):
    """When lookup misses but the same text exists with a different context hash, log a debug message."""
    s, mock_conn, mock_cursor = store
    # First query (lookup by content_hash) returns None
    mock_cursor.fetchone.side_effect = [None, ("exists",)]
    with patch("transduck.shared_store.logger") as mock_logger:
        result = s.lookup(
            source_text="Hello",
            source_lang="EN",
            target_lang="DE",
            project_context_hash=_hash("ctx-a"),
            string_context_hash=_hash(""),
        )
    assert result is None
    mock_logger.debug.assert_called_once()
    msg = mock_logger.debug.call_args[0][0]
    assert "different project context hash" in msg


def test_lookup_miss_no_log_when_no_alt_exists(store):
    """When lookup misses and no alternative context exists, no debug message."""
    s, mock_conn, mock_cursor = store
    mock_cursor.fetchone.side_effect = [None, None]
    with patch("transduck.shared_store.logger") as mock_logger:
        result = s.lookup(
            source_text="Hello",
            source_lang="EN",
            target_lang="DE",
            project_context_hash=_hash("ctx-a"),
            string_context_hash=_hash(""),
        )
    assert result is None
    mock_logger.debug.assert_not_called()


def test_import_error_without_psycopg2():
    """SharedStore raises ImportError if psycopg2 is not installed."""
    with patch.dict("sys.modules", {"psycopg2": None}):
        from importlib import reload
        import transduck.shared_store as mod
        reload(mod)
        with pytest.raises(ImportError, match="psycopg2"):
            mod.SharedStore("postgresql://localhost/test")
