# #region TranslateHistoryTests [C:2] [TYPE Module]
# @SEMANTICS: tests, translate, history, metrics
# @PURPOSE: Tests for run history list/detail endpoints and metrics aggregation.
# @LAYER Tests
# @RELATION  BINDS_TO -> [TranslateRoutes]
# @RELATION  BINDS_TO -> [TranslationMetrics]
#
# @TEST_CONTRACT: HistoryFlow ->
# {
#   invariants: [
#       "GET /api/translate/runs returns paginated list",
#       "GET /api/translate/runs supports filters (job_id, status, trigger_type)",
#       "GET /api/translate/runs/{id}/detail returns config_snapshot, records, events",
#       "GET /api/translate/jobs/{id}/metrics returns aggregated data",
#       "Metrics include run counts, record counts, duration"
#   ]
# }
# @TEST_FIXTURE: completed_run -> run with COMPLETED status
# @TEST_EDGE: filter_by_job_id -> returns only that job's runs
# @TEST_EDGE: filter_by_status -> returns only matching status
# @TEST_EDGE: metrics_empty_job -> returns zero counts
# @TEST_INVARIANT: history_list_and_detail -> VERIFIED_BY: [completed_run, filter_by_status]

from pathlib import Path
import sys

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

from datetime import UTC, datetime
import pytest
from unittest.mock import MagicMock
import uuid

from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from src.app import app
from src.core.database import Base
from src.dependencies import get_config_manager, get_current_user, get_db
from src.models.translate import TranslationEvent, TranslationJob, TranslationRun

# 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)

mock_user = MagicMock()
mock_user.username = "testuser"
mock_user.roles = []
admin_role = MagicMock()
admin_role.name = "Admin"
mock_user.roles.append(admin_role)


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


@pytest.fixture
def mock_api_deps():
    config_manager = MagicMock()
    config_manager.get_environments.return_value = []

    connection = engine.connect()
    transaction = connection.begin()
    session = TestingSessionLocal(bind=connection)

    def override_get_db():
        try:
            yield session
        finally:
            pass

    overrides = {
        get_current_user: lambda: mock_user,
        get_config_manager: lambda: config_manager,
        get_db: override_get_db,
    }
    for dep, override in overrides.items():
        app.dependency_overrides[dep] = override

    yield {"config_manager": config_manager, "session": session}

    app.dependency_overrides.clear()
    session.close()
    transaction.rollback()
    connection.close()


@pytest.fixture
def client(mock_api_deps):
    return TestClient(app)


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


def _create_test_run(db_session, job_id: str, status: str = "COMPLETED", trigger: str = "manual") -> TranslationRun:
    run = TranslationRun(
        id=str(uuid.uuid4()),
        job_id=job_id,
        status=status,
        trigger_type=trigger,
        total_records=100,
        successful_records=80,
        failed_records=10,
        skipped_records=10,
        config_snapshot={"test": True},
        config_hash="abc123",
        dict_snapshot_hash="def456",
        key_hash="ghi789",
        created_by="testuser",
        created_at=datetime.now(UTC),
    )
    db_session.add(run)
    db_session.flush()
    return run


# #region test_list_runs_empty [C:2] [TYPE Function]
# @PURPOSE: Verify runs list returns empty result initially.
def test_list_runs_empty(client):
    """Test runs list initially returns empty."""
    response = client.get("/api/translate/runs")
    assert response.status_code == 200
    data = response.json()
    assert data["total"] == 0
    assert data["items"] == []
# #endregion test_list_runs_empty


# #region test_list_runs_with_data [C:2] [TYPE Function]
# @PURPOSE: Verify runs list returns data when runs exist.
def test_list_runs_with_data(client, mock_api_deps):
    """Test runs list with existing runs."""
    session = mock_api_deps["session"]
    job = _create_test_job(session)
    _create_test_run(session, job.id)
    session.commit()

    response = client.get("/api/translate/runs")
    assert response.status_code == 200
    data = response.json()
    assert data["total"] >= 1
    assert len(data["items"]) >= 1
    assert data["items"][0]["job_id"] == job.id
# #endregion test_list_runs_with_data


# #region test_list_runs_filter_job_id [C:2] [TYPE Function]
# @PURPOSE: Verify filtering runs by job_id works.
def test_list_runs_filter_job_id(client, mock_api_deps):
    """Test filtering runs by job_id."""
    session = mock_api_deps["session"]
    job1 = _create_test_job(session)
    job2 = _create_test_job(session)
    _create_test_run(session, job1.id)
    _create_test_run(session, job2.id)
    session.commit()

    response = client.get(f"/api/translate/runs?job_id={job1.id}")
    assert response.status_code == 200
    data = response.json()
    assert data["total"] >= 1
    for item in data["items"]:
        assert item["job_id"] == job1.id
# #endregion test_list_runs_filter_job_id


# #region test_list_runs_filter_status [C:2] [TYPE Function]
# @PURPOSE: Verify filtering runs by status works.
def test_list_runs_filter_status(client, mock_api_deps):
    """Test filtering runs by status."""
    session = mock_api_deps["session"]
    job = _create_test_job(session)
    _create_test_run(session, job.id, status="COMPLETED")
    _create_test_run(session, job.id, status="FAILED")
    session.commit()

    response = client.get("/api/translate/runs?run_status=FAILED")
    assert response.status_code == 200
    data = response.json()
    for item in data["items"]:
        assert item["status"] == "FAILED"
# #endregion test_list_runs_filter_status


# #region test_get_run_detail [C:2] [TYPE Function]
# @PURPOSE: Verify run detail returns config_snapshot, records, events.
def test_get_run_detail(client, mock_api_deps):
    """Test run detail endpoint."""
    session = mock_api_deps["session"]
    job = _create_test_job(session)
    run = _create_test_run(session, job.id)

    # Add an event
    event = TranslationEvent(
        id=str(uuid.uuid4()),
        job_id=job.id,
        run_id=run.id,
        event_type="RUN_STARTED",
        event_data={},
        created_by="testuser",
        created_at=datetime.now(UTC),
    )
    session.add(event)
    session.commit()

    response = client.get(f"/api/translate/runs/{run.id}/detail")
    assert response.status_code == 200
    data = response.json()
    assert data["id"] == run.id
    assert data["config_snapshot"] == {"test": True}
    assert data["config_hash"] == "abc123"
    assert len(data["events"]) >= 1
    assert data["events"][0]["event_type"] == "RUN_STARTED"
# #endregion test_get_run_detail


# #region test_get_job_metrics [C:2] [TYPE Function]
# @PURPOSE: Verify metrics endpoint returns aggregated data.
def test_get_job_metrics(client, mock_api_deps):
    """Test job metrics endpoint."""
    session = mock_api_deps["session"]
    job = _create_test_job(session)
    _create_test_run(session, job.id, status="COMPLETED")
    _create_test_run(session, job.id, status="FAILED")
    session.commit()

    response = client.get(f"/api/translate/jobs/{job.id}/metrics")
    assert response.status_code == 200
    data = response.json()
    assert data["job_id"] == job.id
    assert data["total_runs"] >= 2
    assert data["successful_runs"] >= 1
    assert data["failed_runs"] >= 1
# #endregion test_get_job_metrics


# #region test_get_all_metrics [C:2] [TYPE Function]
# @PURPOSE: Verify global metrics endpoint returns data.
def test_get_all_metrics(client, mock_api_deps):
    """Test global metrics endpoint."""
    session = mock_api_deps["session"]
    job = _create_test_job(session)
    _create_test_run(session, job.id)
    session.commit()

    response = client.get("/api/translate/metrics")
    assert response.status_code == 200
    data = response.json()
    assert isinstance(data, list)
    assert len(data) >= 1
# #endregion test_get_all_metrics


# #region test_metrics_empty_job [C:2] [TYPE Function]
# @PURPOSE: Verify metrics for job with no runs returns zeros.
def test_metrics_empty_job(client, mock_api_deps):
    """Test metrics for job with no runs."""
    session = mock_api_deps["session"]
    job = _create_test_job(session)
    session.commit()

    response = client.get(f"/api/translate/jobs/{job.id}/metrics")
    assert response.status_code == 200
    data = response.json()
    assert data["total_runs"] == 0
# #endregion test_metrics_empty_job


# #region test_run_detail_not_found [C:2] [TYPE Function]
# @PURPOSE: Verify 404 on non-existent run detail.
def test_run_detail_not_found(client):
    """Test run detail returns 404 for non-existent run."""
    response = client.get("/api/translate/runs/non-existent/detail")
    assert response.status_code == 404
# #endregion test_run_detail_not_found
# #region test_list_runs_includes_language_info [C:2] [TYPE Function]
# @PURPOSE: Verify run list includes target_languages, total_translated, and language_stats.
def test_list_runs_includes_language_info(client, mock_api_deps):
    """Test run list includes per-language fields."""
    from src.models.translate import TranslationRunLanguageStats
    session = mock_api_deps["session"]
    job = _create_test_job(session)
    run = _create_test_run(session, job.id)
    # Add language stats
    ls = TranslationRunLanguageStats(
        id=str(uuid.uuid4()),
        run_id=run.id,
        language_code="ru",
        total_rows=100,
        translated_rows=80,
        failed_rows=5,
        skipped_rows=15,
        token_count=15000,
        estimated_cost=0.03,
    )
    session.add(ls)
    session.commit()

    response = client.get("/api/translate/runs")
    assert response.status_code == 200
    data = response.json()
    assert len(data["items"]) >= 1
    item = data["items"][0]
    assert "target_languages" in item
    assert "total_translated" in item
    assert "language_stats" in item
    assert "ru" in item["target_languages"]
    assert item["total_translated"] == 80
    assert item["language_stats"]["ru"]["translated_rows"] == 80
# #endregion test_list_runs_includes_language_info


# #region test_metrics_per_language_breakdown [C:2] [TYPE Function]
# @PURPOSE: Verify metrics endpoint returns per_language_metrics.
def test_metrics_per_language_breakdown(client, mock_api_deps):
    """Test metrics returns per-language breakdown."""
    from src.models.translate import TranslationRunLanguageStats
    session = mock_api_deps["session"]
    job = _create_test_job(session)
    run = _create_test_run(session, job.id)
    ls = TranslationRunLanguageStats(
        id=str(uuid.uuid4()),
        run_id=run.id,
        language_code="ru",
        total_rows=100,
        translated_rows=80,
        token_count=15000,
        estimated_cost=0.03,
    )
    session.add(ls)
    session.commit()

    response = client.get(f"/api/translate/jobs/{job.id}/metrics")
    assert response.status_code == 200
    data = response.json()
    assert "per_language_metrics" in data
    assert "ru" in data["per_language_metrics"]
    assert data["per_language_metrics"]["ru"]["tokens"] == 15000
    assert data["per_language_metrics"]["ru"]["cost"] == 0.03
# #endregion test_metrics_per_language_breakdown


# #region test_metric_snapshot_stores_per_language [C:2] [TYPE Function]
# @PURPOSE: Verify MetricSnapshot stores per_language_metrics via prune_expired.
def test_metric_snapshot_stores_per_language(client, mock_api_deps):
    """Test MetricSnapshot stores per_language_metrics after prune."""
    from datetime import timedelta

    from src.models.translate import MetricSnapshot, TranslationEvent
    from src.plugins.translate.events import TranslationEventLog

    session = mock_api_deps["session"]
    job = _create_test_job(session)
    session.commit()

    # Create an old event with per-language data
    old_date = datetime.now(UTC) - timedelta(days=100)
    event = TranslationEvent(
        id=str(uuid.uuid4()),
        job_id=job.id,
        event_type="TRANSLATION_PHASE_COMPLETED",
        event_data={
            "language_code": "ru",
            "token_count": 5000,
            "cost": 0.01,
            "run_id": "test-run-id",
        },
        created_at=old_date,
    )
    session.add(event)
    session.commit()

    # Prune events older than 30 days
    event_log = TranslationEventLog(session)
    result = event_log.prune_expired(retention_days=30)
    assert result["snapshot_id"] is not None

    # Verify snapshot has per_language_metrics
    snapshot = session.query(MetricSnapshot).filter(
        MetricSnapshot.id == result["snapshot_id"]
    ).first()
    assert snapshot is not None
    assert snapshot.per_language_metrics is not None
    assert "ru" in snapshot.per_language_metrics
    assert snapshot.per_language_metrics["ru"]["cumulative_tokens"] == 5000
    assert snapshot.per_language_metrics["ru"]["cumulative_cost"] == 0.01
# #endregion test_metric_snapshot_stores_per_language


# #region test_combined_metrics_live_and_snapshot [C:2] [TYPE Function]
# @PURPOSE: Verify combined metrics merge live + snapshot per-language data.
def test_combined_metrics_live_and_snapshot(client, mock_api_deps):
    """Test metrics merge live data with MetricSnapshot per-language data."""
    from datetime import timedelta

    from src.models.translate import (
        MetricSnapshot,
        TranslationRunLanguageStats,
    )
    from src.plugins.translate.metrics import TranslationMetrics

    session = mock_api_deps["session"]
    job = _create_test_job(session)
    run = _create_test_run(session, job.id)
    session.commit()

    # Add snapshot with per-language data (simulating pruned period)
    snapshot = MetricSnapshot(
        id=str(uuid.uuid4()),
        job_id=job.id,
        key_hash="test_key",
        per_language_metrics={
            "ru": {"cumulative_tokens": 100000, "cumulative_cost": 0.20, "runs": 3},
        },
        covers_events_before=datetime.now(UTC) - timedelta(days=90),
        total_records=100,
        snapshot_date=datetime.now(UTC),
    )
    session.add(snapshot)

    # Add live language stats (recent runs)
    ls = TranslationRunLanguageStats(
        id=str(uuid.uuid4()),
        run_id=run.id,
        language_code="ru",
        total_rows=50,
        translated_rows=40,
        token_count=5000,
        estimated_cost=0.01,
    )
    session.add(ls)
    session.commit()

    metrics_svc = TranslationMetrics(session)
    result = metrics_svc.get_job_metrics(job.id)
    assert "per_language_metrics" in result
    assert "ru" in result["per_language_metrics"]
    # Live 5000 + snapshot 100000 = 105000
    assert result["per_language_metrics"]["ru"]["tokens"] == 105000
    # Live 0.01 + snapshot 0.20 = 0.21
    assert abs(result["per_language_metrics"]["ru"]["cost"] - 0.21) < 0.001
    # Live 1 + snapshot 3 = 4
    assert result["per_language_metrics"]["ru"]["runs"] == 4
# #endregion test_combined_metrics_live_and_snapshot
# #endregion TranslateHistoryTests
