# #region DictSnapshotHashTests [C:2] [TYPE Module] [SEMANTICS test, translate, hash, dictionary, cache]
# @BRIEF Tests for compute_dict_snapshot_hash — verifies hash changes when
#         dictionary entries are modified (invalidates translation cache).
# @LAYER Test

import os

# Must set DATABASE_URL before importing any src modules
os.environ["DATABASE_URL"] = "sqlite:///:memory:"
os.environ["AUTH_DATABASE_URL"] = "sqlite:///:memory:"

from datetime import UTC, datetime, timedelta
import hashlib
import time
import uuid

import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from src.core.database import Base
from src.models.translate import (
    DictionaryEntry,
    TerminologyDictionary,
    TranslationJob,
    TranslationJobDictionary,
)
from src.plugins.translate.orchestrator_config import compute_dict_snapshot_hash
from src.plugins.translate.preview_response_parser import compute_dict_snapshot_hash as preview_dict_hash

# ── In-memory SQLite ───────────────────────────────────────────────
SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base.metadata.create_all(bind=engine)


@pytest.fixture
def db_session():
    connection = engine.connect()
    transaction = connection.begin()
    session = TestingSessionLocal(bind=connection)
    yield session
    session.close()
    transaction.rollback()
    connection.close()


def _create_job(db_session) -> TranslationJob:
    job = TranslationJob(
        id=str(uuid.uuid4()),
        name="Hash Test Job",
        source_dialect="postgresql",
        target_dialect="clickhouse",
        status="ACTIVE",
    )
    db_session.add(job)
    db_session.flush()
    return job


def _create_dict(db_session, name="Test Dict") -> TerminologyDictionary:
    d = TerminologyDictionary(
        id=str(uuid.uuid4()),
        name=name,
    )
    db_session.add(d)
    db_session.flush()
    return d


def _create_entry(db_session, dict_id: str, source: str, target: str,
                  src_lang: str = "ru", tgt_lang: str = "en") -> DictionaryEntry:
    e = DictionaryEntry(
        id=str(uuid.uuid4()),
        dictionary_id=dict_id,
        source_term=source,
        source_term_normalized=source.lower(),
        target_term=target,
        source_language=src_lang,
        target_language=tgt_lang,
    )
    db_session.add(e)
    db_session.flush()
    return e


def _link_dict(db_session, job_id: str, dict_id: str):
    link = TranslationJobDictionary(
        id=str(uuid.uuid4()),
        job_id=job_id,
        dictionary_id=dict_id,
    )
    db_session.add(link)
    db_session.flush()


# ── Tests ──────────────────────────────────────────────────────────


class TestDictSnapshotHash:
    """Tests for compute_dict_snapshot_hash — both orchestrator and preview versions."""

    def test_no_dicts_returns_non_empty_hash(self, db_session):
        job = _create_job(db_session)
        h = compute_dict_snapshot_hash(db_session, job.id)
        assert len(h) == 16
        assert h == hashlib.sha256(b"").hexdigest()[:16]

    def test_preview_no_dicts_returns_non_empty_hash(self, db_session):
        job = _create_job(db_session)
        h = preview_dict_hash(db_session, job.id)
        assert len(h) == 16

    def test_hash_changes_when_entry_added(self, db_session):
        job = _create_job(db_session)
        d = _create_dict(db_session)
        _link_dict(db_session, job.id, d.id)

        h_before = compute_dict_snapshot_hash(db_session, job.id)

        # Add an entry
        _create_entry(db_session, d.id, "привет", "hello")
        db_session.commit()

        h_after = compute_dict_snapshot_hash(db_session, job.id)
        assert h_before != h_after, "Hash should change when entry is added"

    def test_hash_changes_when_entry_modified(self, db_session):
        job = _create_job(db_session)
        d = _create_dict(db_session)
        _link_dict(db_session, job.id, d.id)
        entry = _create_entry(db_session, d.id, "привет", "hello")
        db_session.commit()

        h_before = compute_dict_snapshot_hash(db_session, job.id)

        # Modify the entry — updated_at changes
        time.sleep(0.01)  # Ensure timestamp difference
        entry.target_term = "hi"
        entry.updated_at = datetime.now(UTC)
        db_session.commit()

        h_after = compute_dict_snapshot_hash(db_session, job.id)
        assert h_before != h_after, "Hash should change when entry target_term is modified"

    def test_hash_changes_when_entry_removed(self, db_session):
        job = _create_job(db_session)
        d = _create_dict(db_session)
        _link_dict(db_session, job.id, d.id)
        entry = _create_entry(db_session, d.id, "привет", "hello")
        db_session.commit()

        h_before = compute_dict_snapshot_hash(db_session, job.id)

        # Remove the entry
        db_session.delete(entry)
        db_session.commit()

        h_after = compute_dict_snapshot_hash(db_session, job.id)
        assert h_before != h_after, "Hash should change when entry is removed"

    def test_hash_changes_when_second_dict_added(self, db_session):
        job = _create_job(db_session)
        d1 = _create_dict(db_session, "Dict 1")
        _link_dict(db_session, job.id, d1.id)
        _create_entry(db_session, d1.id, "привет", "hello")
        db_session.commit()

        h_before = compute_dict_snapshot_hash(db_session, job.id)

        # Add second dictionary with entries
        d2 = _create_dict(db_session, "Dict 2")
        _link_dict(db_session, job.id, d2.id)
        _create_entry(db_session, d2.id, "мир", "world")
        db_session.commit()

        h_after = compute_dict_snapshot_hash(db_session, job.id)
        assert h_before != h_after, "Hash should change when second dictionary is added"

    def test_hash_deterministic_for_same_state(self, db_session):
        job = _create_job(db_session)
        d = _create_dict(db_session)
        _link_dict(db_session, job.id, d.id)
        _create_entry(db_session, d.id, "привет", "hello")
        _create_entry(db_session, d.id, "мир", "world")
        db_session.commit()

        h1 = compute_dict_snapshot_hash(db_session, job.id)
        h2 = compute_dict_snapshot_hash(db_session, job.id)

        assert h1 == h2, "Hash should be deterministic for the same state"

    def test_hash_stable_when_no_entries_changed(self, db_session):
        job = _create_job(db_session)
        d = _create_dict(db_session)
        _link_dict(db_session, job.id, d.id)
        _create_entry(db_session, d.id, "привет", "hello")
        db_session.commit()

        h_before = compute_dict_snapshot_hash(db_session, job.id)

        # Touch the job (not the dictionary) — hash should stay same
        job.name = "Renamed Job"
        db_session.commit()

        h_after = compute_dict_snapshot_hash(db_session, job.id)
        assert h_before == h_after, "Hash should be stable when only non-dict data changes"

    def test_both_implementations_produce_same_hash(self, db_session):
        job = _create_job(db_session)
        d = _create_dict(db_session)
        _link_dict(db_session, job.id, d.id)
        _create_entry(db_session, d.id, "привет", "hello")
        db_session.commit()

        h1 = compute_dict_snapshot_hash(db_session, job.id)
        h2 = preview_dict_hash(db_session, job.id)

        assert h1 == h2, "Orchestrator and preview implementations should produce same hash"

    def test_hash_with_multiple_entries_tracks_latest_update(self, db_session):
        job = _create_job(db_session)
        d = _create_dict(db_session)
        _link_dict(db_session, job.id, d.id)
        e1 = _create_entry(db_session, d.id, "привет", "hello")
        e2 = _create_entry(db_session, d.id, "мир", "world")
        db_session.commit()

        h_before = compute_dict_snapshot_hash(db_session, job.id)

        # Update only the second entry
        time.sleep(0.01)
        e2.target_term = "planet"
        db_session.commit()

        h_after = compute_dict_snapshot_hash(db_session, job.id)
        assert h_before != h_after, "Hash should change when any entry is modified"

# #endregion DictSnapshotHashTests
