# #region Test.OrchestratorConfig [C:3] [TYPE Module] [SEMANTICS test, translate, config, hash, snapshot]
# @BRIEF Tests for orchestrator_config.py — compute_config_hash, compute_dict_snapshot_hash.
# @RELATION BINDS_TO -> [orchestrator_config]
# @TEST_CONTRACT: compute_config_hash -> str | deterministic 16-char hash from job config
# @TEST_CONTRACT: compute_dict_snapshot_hash -> str | hash from dictionary entries

import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))

import os
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-testing")
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:")

from datetime import datetime, timezone
from unittest.mock import MagicMock, patch

import pytest

from src.plugins.translate.orchestrator_config import (
    compute_config_hash,
    compute_dict_snapshot_hash,
)


class TestComputeConfigHash:
    """compute_config_hash — deterministic hash from job config."""

    def _make_job(self, overrides: dict | None = None) -> MagicMock:
        """Create a mock job with default config."""
        job = MagicMock()
        job.source_dialect = "postgresql"
        job.target_dialect = "clickhouse"
        job.source_datasource_id = "ds-1"
        job.translation_column = "name"
        job.context_columns = ["desc"]
        job.target_languages = ["ru", "de"]
        job.provider_id = "provider-1"
        job.batch_size = 50
        job.upsert_strategy = "MERGE"
        if overrides:
            for k, v in overrides.items():
                setattr(job, k, v)
        return job

    def test_hash_length(self):
        """Hash is 16 characters."""
        job = self._make_job()
        h = compute_config_hash(job)
        assert isinstance(h, str)
        assert len(h) == 16

    def test_deterministic(self):
        """Same config produces same hash."""
        job1 = self._make_job()
        job2 = self._make_job()
        assert compute_config_hash(job1) == compute_config_hash(job2)

    def test_different_config_different_hash(self):
        """Different config produces different hash."""
        job1 = self._make_job({"batch_size": 50})
        job2 = self._make_job({"batch_size": 100})
        assert compute_config_hash(job1) != compute_config_hash(job2)

    def test_source_dialect_affects_hash(self):
        """Changing source_dialect changes hash."""
        job1 = self._make_job({"source_dialect": "postgresql"})
        job2 = self._make_job({"source_dialect": "mysql"})
        assert compute_config_hash(job1) != compute_config_hash(job2)

    def test_target_languages_sorted(self):
        """target_languages are sorted before hashing."""
        job1 = self._make_job({"target_languages": ["de", "ru"]})
        job2 = self._make_job({"target_languages": ["ru", "de"]})
        assert compute_config_hash(job1) == compute_config_hash(job2)

    def test_target_languages_empty(self):
        """Empty target_languages handled."""
        job = self._make_job({"target_languages": None})
        h = compute_config_hash(job)
        assert isinstance(h, str)
        assert len(h) == 16


class TestComputeDictSnapshotHash:
    """compute_dict_snapshot_hash — dictionary state hash."""

    def test_no_dictionary_links(self):
        """No dictionary links returns hash of empty bytes."""
        db = MagicMock()
        db.query.return_value.filter.return_value.all.return_value = []
        h = compute_dict_snapshot_hash(db, "job-1")
        assert isinstance(h, str)
        assert len(h) == 16

    def test_with_dictionary_links(self):
        """Dictionary links with entries produce a hash."""
        db = MagicMock()
        mock_link = MagicMock()
        mock_link.dictionary_id = "dict-1"
        db.query.return_value.filter.return_value.all.return_value = [mock_link]
        db.query.return_value.filter.return_value.scalar.return_value = 5  # entry_count
        db.query.return_value.filter.return_value.scalar.side_effect = [
            5,  # entry_count for dict-1
            datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc),  # max_updated
        ]
        h = compute_dict_snapshot_hash(db, "job-1")
        assert isinstance(h, str)
        assert len(h) == 16

    def test_deterministic_with_same_data(self):
        """Same data produces same hash."""
        db1 = MagicMock()
        db2 = MagicMock()
        mock_link = MagicMock()
        mock_link.dictionary_id = "dict-1"
        db1.query.return_value.filter.return_value.all.return_value = [mock_link]
        db2.query.return_value.filter.return_value.all.return_value = [mock_link]
        db1.query.return_value.filter.return_value.scalar.side_effect = [5, datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc)]
        db2.query.return_value.filter.return_value.scalar.side_effect = [5, datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc)]
        h1 = compute_dict_snapshot_hash(db1, "job-1")
        h2 = compute_dict_snapshot_hash(db2, "job-1")
        assert h1 == h2

    def test_different_entry_count_changes_hash(self):
        """Different entry count produces different hash."""
        db1 = MagicMock()
        db2 = MagicMock()
        mock_link = MagicMock()
        mock_link.dictionary_id = "dict-1"
        db1.query.return_value.filter.return_value.all.return_value = [mock_link]
        db2.query.return_value.filter.return_value.all.return_value = [mock_link]
        db1.query.return_value.filter.return_value.scalar.side_effect = [5, datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc)]
        db2.query.return_value.filter.return_value.scalar.side_effect = [10, datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc)]
        h1 = compute_dict_snapshot_hash(db1, "job-1")
        h2 = compute_dict_snapshot_hash(db2, "job-1")
        assert h1 != h2

    def test_max_updated_none_handled(self):
        """When max_updated is None, '0' is used."""
        db = MagicMock()
        mock_link = MagicMock()
        mock_link.dictionary_id = "dict-1"
        db.query.return_value.filter.return_value.all.return_value = [mock_link]
        db.query.return_value.filter.return_value.scalar.side_effect = [5, None]
        h = compute_dict_snapshot_hash(db, "job-1")
        assert isinstance(h, str)
        assert len(h) == 16

    def test_multiple_dictionaries(self):
        """Multiple dictionary links handled."""
        db = MagicMock()
        link1 = MagicMock()
        link1.dictionary_id = "dict-a"
        link2 = MagicMock()
        link2.dictionary_id = "dict-b"
        db.query.return_value.filter.return_value.all.return_value = [link2, link1]  # unsorted
        db.query.return_value.filter.return_value.scalar.side_effect = [
            3, datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc),  # dict-a
            7, datetime(2026, 2, 1, 8, 0, 0, tzinfo=timezone.utc),   # dict-b
        ]
        h = compute_dict_snapshot_hash(db, "job-1")
        assert isinstance(h, str)
        assert len(h) == 16
# #endregion Test.OrchestratorConfig
