# #region Test.Metrics [C:3] [TYPE Module] [SEMANTICS test, translate, metrics, job, statistics]
# @BRIEF Tests for metrics.py — TranslationMetrics.get_job_metrics, get_all_metrics.
# @RELATION BINDS_TO -> [TranslationMetrics]
# @TEST_CONTRACT: get_job_metrics -> dict | aggregated job metrics
# @TEST_CONTRACT: get_all_metrics -> list[dict] | aggregated metrics for all jobs
# @TEST_EDGE: no_runs
# @TEST_EDGE: per_language_aggregation
# @TEST_EDGE: metric_snapshot_merge

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.metrics import TranslationMetrics


class _MockDB:
    """Helper to create a DB mock with independent query mocks per call."""

    @staticmethod
    def create(query_count: int = 8):
        """Create db mock where each db.query() call returns an independent mock."""
        db = MagicMock()
        query_mocks = [MagicMock() for _ in range(query_count)]
        db.query.side_effect = query_mocks
        return db, query_mocks


class TestGetJobMetrics:
    """TranslationMetrics.get_job_metrics — aggregated job metrics."""

    def _make_lang_stat(self, code: str, **kwargs):
        ls = MagicMock()
        ls.language_code = code
        ls.token_count = 1000
        ls.estimated_cost = 0.002
        ls.translated_rows = 50
        for k, v in kwargs.items():
            setattr(ls, k, v)
        return ls

    def test_basic_metrics(self):
        """Returns dict with all required keys."""
        db, q = _MockDB.create()

        # 0: run_counts — group_by().all()
        q[0].filter.return_value.group_by.return_value.all.return_value = [
            ("COMPLETED", 5), ("FAILED", 2),
        ]
        # 1: record_stats — first()
        q[1].filter.return_value.first.return_value = (500, 480, 10, 10)
        # 2: avg_duration — filter(cond1, cond2, cond3).scalar() — single filter with 3 args
        q[2].filter.return_value.scalar.return_value = 120000.0
        # 3: last_run — filter().order_by().first()
        last_run = MagicMock()
        last_run.created_at = datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
        q[3].filter.return_value.order_by.return_value.first.return_value = last_run
        # 4: next_schedule — filter().first()
        next_schedule = MagicMock()
        next_schedule.last_run_at = datetime(2026, 1, 20, 12, 0, 0, tzinfo=timezone.utc)
        q[4].filter.return_value.first.return_value = next_schedule
        # 5: latest_snapshot (1st call) — filter().order_by().first()
        q[5].filter.return_value.order_by.return_value.first.return_value = None
        # 6: lang_stats_rows — join().filter().all()
        lang_stats = [self._make_lang_stat("ru"), self._make_lang_stat("de")]
        q[6].join.return_value.filter.return_value.all.return_value = lang_stats
        # 7: latest_snapshot (2nd call, same query) — filter().order_by().first()
        q[7].filter.return_value.order_by.return_value.first.return_value = None

        metrics = TranslationMetrics(db)
        result = metrics.get_job_metrics("job-1")

        assert result["job_id"] == "job-1"
        assert result["total_runs"] == 7
        assert result["successful_runs"] == 5
        assert result["failed_runs"] == 2
        assert result["cancelled_runs"] == 0
        assert result["total_records"] == 500
        assert result["successful_records"] == 480
        assert result["failed_records"] == 10
        assert result["skipped_records"] == 10
        assert result["cumulative_tokens"] == 2000
        assert result["cumulative_cost"] == 0.004
        assert result["avg_duration_ms"] == 120000
        assert result["last_run_at"] is not None
        assert result["next_scheduled_run"] is not None
        assert len(result["per_language_metrics"]) == 2
        assert "ru" in result["per_language_metrics"]
        assert "de" in result["per_language_metrics"]

    def test_no_runs(self):
        """No runs returns zero metrics."""
        db, q = _MockDB.create()

        q[0].filter.return_value.group_by.return_value.all.return_value = []
        q[1].filter.return_value.first.return_value = (0, 0, 0, 0)
        q[2].filter.return_value.scalar.return_value = None
        q[3].filter.return_value.order_by.return_value.first.return_value = None
        q[4].filter.return_value.first.return_value = None
        q[5].filter.return_value.order_by.return_value.first.return_value = None
        q[6].join.return_value.filter.return_value.all.return_value = []
        q[7].filter.return_value.order_by.return_value.first.return_value = None

        metrics = TranslationMetrics(db)
        result = metrics.get_job_metrics("job-1")

        assert result["total_runs"] == 0
        assert result["total_records"] == 0
        assert result["avg_duration_ms"] is None
        assert result["last_run_at"] is None
        assert result["next_scheduled_run"] is None
        assert result["per_language_metrics"] == {}

    def test_avg_duration_none_when_no_timed_runs(self):
        """No completed runs => avg_duration_ms is None."""
        db, q = _MockDB.create()

        q[0].filter.return_value.group_by.return_value.all.return_value = [("PENDING", 1)]
        q[1].filter.return_value.first.return_value = (0, 0, 0, 0)
        q[2].filter.return_value.scalar.return_value = None
        q[3].filter.return_value.order_by.return_value.first.return_value = None
        q[4].filter.return_value.first.return_value = None
        q[5].filter.return_value.order_by.return_value.first.return_value = None
        q[6].join.return_value.filter.return_value.all.return_value = []
        q[7].filter.return_value.order_by.return_value.first.return_value = None

        metrics = TranslationMetrics(db)
        result = metrics.get_job_metrics("job-1")

        assert result["avg_duration_ms"] is None

    def test_no_next_schedule(self):
        """No active schedule => next_scheduled_run is None."""
        db, q = _MockDB.create()

        q[0].filter.return_value.group_by.return_value.all.return_value = []
        q[1].filter.return_value.first.return_value = (0, 0, 0, 0)
        q[2].filter.return_value.scalar.return_value = None
        q[3].filter.return_value.order_by.return_value.first.return_value = None
        q[4].filter.return_value.first.return_value = None
        q[5].filter.return_value.order_by.return_value.first.return_value = None
        q[6].join.return_value.filter.return_value.all.return_value = []
        q[7].filter.return_value.order_by.return_value.first.return_value = None

        metrics = TranslationMetrics(db)
        result = metrics.get_job_metrics("job-1")

        assert result["next_scheduled_run"] is None

    def test_per_language_with_snapshot_merge(self):
        """MetricSnapshot per_language_metrics merged into result."""
        db, q = _MockDB.create()

        q[0].filter.return_value.group_by.return_value.all.return_value = [("COMPLETED", 3)]
        q[1].filter.return_value.first.return_value = (100, 95, 3, 2)
        q[2].filter.return_value.scalar.return_value = 50000.0
        q[3].filter.return_value.order_by.return_value.first.return_value = None  # no last_run
        q[4].filter.return_value.first.return_value = None  # no next_schedule
        # 5: latest_snapshot (1st)
        snapshot1 = MagicMock()
        snapshot1.per_language_metrics = None
        q[5].filter.return_value.order_by.return_value.first.return_value = snapshot1
        # 6: lang_stats_rows (empty)
        q[6].join.return_value.filter.return_value.all.return_value = []
        # 7: latest_snapshot (2nd) with per_language_metrics
        snapshot2 = MagicMock()
        snapshot2.per_language_metrics = {
            "ru": {"cumulative_tokens": 5000, "cumulative_cost": 0.01, "runs": 10},
        }
        q[7].filter.return_value.order_by.return_value.first.return_value = snapshot2

        metrics = TranslationMetrics(db)
        result = metrics.get_job_metrics("job-1")

        assert "ru" in result["per_language_metrics"]
        assert result["cumulative_tokens"] == 5000
        assert result["cumulative_cost"] == 0.01

    def test_per_language_with_live_and_snapshot(self):
        """Live stats + snapshot metrics merge correctly."""
        db, q = _MockDB.create()

        q[0].filter.return_value.group_by.return_value.all.return_value = [("COMPLETED", 3)]
        q[1].filter.return_value.first.return_value = (100, 95, 3, 2)
        q[2].filter.return_value.scalar.return_value = 50000.0
        q[3].filter.return_value.order_by.return_value.first.return_value = None  # no last_run
        q[4].filter.return_value.first.return_value = None  # no next_schedule
        # 5: latest_snapshot (1st)
        q[5].filter.return_value.order_by.return_value.first.return_value = None
        # 6: lang_stats_rows — live data
        q[6].join.return_value.filter.return_value.all.return_value = [
            self._make_lang_stat("ru", token_count=500, estimated_cost=0.001, translated_rows=25),
            self._make_lang_stat("fr", token_count=200, estimated_cost=0.0005, translated_rows=10),
        ]
        # 7: latest_snapshot (2nd)
        snapshot = MagicMock()
        snapshot.per_language_metrics = {
            "ru": {"cumulative_tokens": 5000, "cumulative_cost": 0.01, "runs": 10},
            "de": {"cumulative_tokens": 3000, "cumulative_cost": 0.006, "runs": 5},
        }
        q[7].filter.return_value.order_by.return_value.first.return_value = snapshot

        metrics = TranslationMetrics(db)
        result = metrics.get_job_metrics("job-1")

        assert "ru" in result["per_language_metrics"]
        assert "de" in result["per_language_metrics"]
        assert "fr" in result["per_language_metrics"]
        # ru: live 500 + snapshot 5000 = 5500
        assert result["per_language_metrics"]["ru"]["tokens"] == 5500
        assert result["per_language_metrics"]["ru"]["cost"] == 0.011
        assert result["per_language_metrics"]["fr"]["tokens"] == 200
        assert result["per_language_metrics"]["de"]["tokens"] == 3000

    def test_no_last_run_returns_none(self):
        """No last run => last_run_at is None."""
        db, q = _MockDB.create()

        q[0].filter.return_value.group_by.return_value.all.return_value = []
        q[1].filter.return_value.first.return_value = (0, 0, 0, 0)
        q[2].filter.return_value.scalar.return_value = None
        q[3].filter.return_value.order_by.return_value.first.return_value = None
        q[4].filter.return_value.first.return_value = None
        q[5].filter.return_value.order_by.return_value.first.return_value = None
        q[6].join.return_value.filter.return_value.all.return_value = []
        q[7].filter.return_value.order_by.return_value.first.return_value = None

        metrics = TranslationMetrics(db)
        result = metrics.get_job_metrics("job-1")

        assert result["last_run_at"] is None


class TestGetAllMetrics:
    """TranslationMetrics.get_all_metrics — metrics for all jobs."""

    def test_all_metrics(self):
        """Returns list of per-job metrics."""
        db = MagicMock()
        db.query.return_value.distinct.return_value.all.return_value = [
            ("job-1",), ("job-2",),
        ]

        with patch.object(TranslationMetrics, "get_job_metrics") as mock_get:
            mock_get.side_effect = [
                {"job_id": "job-1", "total_runs": 5},
                {"job_id": "job-2", "total_runs": 3},
            ]
            metrics = TranslationMetrics(db)
            result = metrics.get_all_metrics()

        assert len(result) == 2
        assert result[0]["job_id"] == "job-1"
        assert result[1]["job_id"] == "job-2"

    def test_empty_jobs(self):
        """No jobs returns empty list."""
        db = MagicMock()
        db.query.return_value.distinct.return_value.all.return_value = []

        metrics = TranslationMetrics(db)
        result = metrics.get_all_metrics()

        assert result == []
# #endregion Test.Metrics
