# -*- coding: utf-8 -*-
"""ETL 状态路由单元测试

覆盖 2 个端点：
- GET /api/etl-status/cursors
- GET /api/etl-status/recent-runs

通过 mock 绕过数据库连接，专注路由逻辑验证。
"""

import os

os.environ.setdefault("JWT_SECRET_KEY", "test-secret-key-for-unit-tests")

from unittest.mock import patch, MagicMock

from fastapi.testclient import TestClient

from app.auth.dependencies import CurrentUser, get_current_user
from app.main import app

_TEST_USER = CurrentUser(user_id=1, site_id=100)


def _override_auth():
    return _TEST_USER


app.dependency_overrides[get_current_user] = _override_auth
client = TestClient(app)

_MOCK_ETL_CONN = "app.routers.etl_status.get_etl_readonly_connection"
_MOCK_APP_CONN = "app.routers.etl_status.get_connection"


def _make_mock_conn(rows):
    """构造 mock 数据库连接，cursor 返回指定行。"""
    mock_conn = MagicMock()
    mock_cur = MagicMock()
    mock_cur.fetchall.return_value = rows
    mock_cur.fetchone.return_value = None
    mock_conn.cursor.return_value.__enter__ = lambda s: mock_cur
    mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
    return mock_conn, mock_cur


# ---------------------------------------------------------------------------
# GET /api/etl-status/cursors
# ---------------------------------------------------------------------------

class TestListCursors:

    @patch(_MOCK_ETL_CONN)
    def test_returns_cursor_list(self, mock_get_conn):
        conn, cur = _make_mock_conn([
            ("ODS_FETCH_ORDERS", "2024-06-15 10:30:00+08", 1500),
            ("ODS_FETCH_MEMBERS", "2024-06-15 09:00:00+08", 800),
        ])
        # fetchone 用于 EXISTS 检查
        cur.fetchone.return_value = (True,)
        mock_get_conn.return_value = conn

        resp = client.get("/api/etl-status/cursors")
        assert resp.status_code == 200
        data = resp.json()
        assert len(data) == 2
        assert data[0]["task_code"] == "ODS_FETCH_ORDERS"
        assert data[0]["last_fetch_time"] == "2024-06-15 10:30:00+08"
        assert data[0]["record_count"] == 1500
        assert data[1]["task_code"] == "ODS_FETCH_MEMBERS"

        # 验证 site_id 传递
        mock_get_conn.assert_called_once_with(_TEST_USER.site_id)
        conn.close.assert_called_once()

    @patch(_MOCK_ETL_CONN)
    def test_table_not_exists_returns_empty(self, mock_get_conn):
        """etl_admin.etl_cursor 表不存在时返回空列表。"""
        conn, cur = _make_mock_conn([])
        cur.fetchone.return_value = (False,)
        mock_get_conn.return_value = conn

        resp = client.get("/api/etl-status/cursors")
        assert resp.status_code == 200
        assert resp.json() == []

    @patch(_MOCK_ETL_CONN)
    def test_null_fields(self, mock_get_conn):
        """游标字段可能为 None（任务从未执行过）。"""
        conn, cur = _make_mock_conn([
            ("ODS_FETCH_INVENTORY", None, None),
        ])
        cur.fetchone.return_value = (True,)
        mock_get_conn.return_value = conn

        resp = client.get("/api/etl-status/cursors")
        assert resp.status_code == 200
        data = resp.json()
        assert data[0]["task_code"] == "ODS_FETCH_INVENTORY"
        assert data[0]["last_fetch_time"] is None
        assert data[0]["record_count"] is None

    @patch(_MOCK_ETL_CONN)
    def test_empty_cursors(self, mock_get_conn):
        """表存在但无数据。"""
        conn, cur = _make_mock_conn([])
        cur.fetchone.return_value = (True,)
        mock_get_conn.return_value = conn

        resp = client.get("/api/etl-status/cursors")
        assert resp.status_code == 200
        assert resp.json() == []


# ---------------------------------------------------------------------------
# GET /api/etl-status/recent-runs
# ---------------------------------------------------------------------------

class TestListRecentRuns:

    @patch(_MOCK_APP_CONN)
    def test_returns_recent_runs(self, mock_get_conn):
        conn, cur = _make_mock_conn([
            (
                "a1b2c3d4-0000-0000-0000-000000000001",
                ["ODS_FETCH_ORDERS", "DWD_LOAD_FROM_ODS"],
                "success",
                "2024-06-15 10:30:00+08",
                "2024-06-15 10:35:00+08",
                300000,
                0,
            ),
            (
                "a1b2c3d4-0000-0000-0000-000000000002",
                ["DWS_AGGREGATE"],
                "failed",
                "2024-06-15 09:00:00+08",
                "2024-06-15 09:01:00+08",
                60000,
                1,
            ),
        ])
        mock_get_conn.return_value = conn

        resp = client.get("/api/etl-status/recent-runs")
        assert resp.status_code == 200
        data = resp.json()
        assert len(data) == 2

        run0 = data[0]
        assert run0["id"] == "a1b2c3d4-0000-0000-0000-000000000001"
        assert run0["task_codes"] == ["ODS_FETCH_ORDERS", "DWD_LOAD_FROM_ODS"]
        assert run0["status"] == "success"
        assert run0["duration_ms"] == 300000
        assert run0["exit_code"] == 0

        run1 = data[1]
        assert run1["status"] == "failed"
        assert run1["exit_code"] == 1

        conn.close.assert_called_once()

    @patch(_MOCK_APP_CONN)
    def test_empty_runs(self, mock_get_conn):
        conn, cur = _make_mock_conn([])
        mock_get_conn.return_value = conn

        resp = client.get("/api/etl-status/recent-runs")
        assert resp.status_code == 200
        assert resp.json() == []

    @patch(_MOCK_APP_CONN)
    def test_null_optional_fields(self, mock_get_conn):
        """正在执行的任务 finished_at / duration_ms / exit_code 为 None。"""
        conn, cur = _make_mock_conn([
            (
                "a1b2c3d4-0000-0000-0000-000000000003",
                ["ODS_FETCH_MEMBERS"],
                "running",
                "2024-06-15 11:00:00+08",
                None,
                None,
                None,
            ),
        ])
        mock_get_conn.return_value = conn

        resp = client.get("/api/etl-status/recent-runs")
        assert resp.status_code == 200
        data = resp.json()
        assert data[0]["status"] == "running"
        assert data[0]["finished_at"] is None
        assert data[0]["duration_ms"] is None
        assert data[0]["exit_code"] is None

    @patch(_MOCK_APP_CONN)
    def test_site_id_filter(self, mock_get_conn):
        """验证查询时传入了正确的 site_id 参数。"""
        conn, cur = _make_mock_conn([])
        mock_get_conn.return_value = conn

        client.get("/api/etl-status/recent-runs")

        # 验证 SQL 中传入了 site_id 和 limit
        call_args = cur.execute.call_args
        params = call_args[0][1]
        assert params[0] == _TEST_USER.site_id
        assert params[1] == 50

    @patch(_MOCK_APP_CONN)
    def test_empty_task_codes(self, mock_get_conn):
        """task_codes 为 None 时应返回空列表。"""
        conn, cur = _make_mock_conn([
            (
                "a1b2c3d4-0000-0000-0000-000000000004",
                None,
                "pending",
                "2024-06-15 12:00:00+08",
                None,
                None,
                None,
            ),
        ])
        mock_get_conn.return_value = conn

        resp = client.get("/api/etl-status/recent-runs")
        assert resp.status_code == 200
        assert resp.json()[0]["task_codes"] == []


# ---------------------------------------------------------------------------
# 认证测试
# ---------------------------------------------------------------------------

class TestEtlStatusAuth:

    def test_requires_auth(self):
        """移除 auth override 后，所有端点应返回 401/403。"""
        original = app.dependency_overrides.pop(get_current_user, None)
        try:
            for url in ["/api/etl-status/cursors", "/api/etl-status/recent-runs"]:
                resp = client.get(url)
                assert resp.status_code in (401, 403), f"GET {url} 应需要认证"
        finally:
            if original:
                app.dependency_overrides[get_current_user] = original
