"""resilience.py 共享容错原语测试"""

from __future__ import annotations

import asyncio
import json
import sqlite3
import time
from pathlib import Path
from unittest.mock import patch

import pytest

from index1.resilience import (
    CircuitBreaker,
    FileStateCircuitBreaker,
    ServiceCooldown,
    mcp_error_boundary,
    safe_db_transaction,
    safe_db_write,
)


# ────────────────────────────────────────────────────
# ServiceCooldown
# ────────────────────────────────────────────────────


class TestServiceCooldown:
    def test_initial_not_cooling(self):
        cd = ServiceCooldown("test")
        assert cd.is_cooling is False
        assert cd.current_backoff == 0.0

    def test_single_failure_starts_cooling(self):
        cd = ServiceCooldown("test", base_cooldown=60.0)
        cd.record_failure()
        assert cd.is_cooling is True
        assert cd.current_backoff == 60.0

    def test_exponential_backoff(self):
        cd = ServiceCooldown("test", base_cooldown=10.0, max_cooldown=100.0, backoff_factor=2.0)
        cd.record_failure()  # 1st: 10s
        assert cd.current_backoff == 10.0
        cd.record_failure()  # 2nd: 20s
        assert cd.current_backoff == 20.0
        cd.record_failure()  # 3rd: 40s
        assert cd.current_backoff == 40.0

    def test_backoff_capped_at_max(self):
        cd = ServiceCooldown("test", base_cooldown=10.0, max_cooldown=50.0, backoff_factor=3.0)
        for _ in range(10):
            cd.record_failure()
        assert cd.current_backoff == 50.0

    def test_success_resets(self):
        cd = ServiceCooldown("test", base_cooldown=10.0)
        cd.record_failure()
        cd.record_failure()
        assert cd.is_cooling is True
        cd.record_success()
        assert cd.is_cooling is False
        assert cd.current_backoff == 0.0
        assert cd._consecutive_failures == 0

    def test_cooldown_expires(self):
        cd = ServiceCooldown("test", base_cooldown=0.05)  # 50ms cooldown
        cd.record_failure()
        assert cd.is_cooling is True
        time.sleep(0.06)
        assert cd.is_cooling is False

    def test_reset(self):
        cd = ServiceCooldown("test")
        cd.record_failure()
        cd.record_failure()
        cd.reset()
        assert cd.is_cooling is False
        assert cd._consecutive_failures == 0

    def test_stats(self):
        cd = ServiceCooldown("ollama")
        stats = cd.stats
        assert stats["name"] == "ollama"
        assert stats["is_cooling"] is False
        assert stats["consecutive_failures"] == 0
        assert stats["last_success_ago_s"] is None

        cd.record_success()
        stats = cd.stats
        assert stats["last_success_ago_s"] is not None

    def test_stats_after_failure(self):
        cd = ServiceCooldown("test", base_cooldown=60.0)
        cd.record_failure()
        stats = cd.stats
        assert stats["is_cooling"] is True
        assert stats["consecutive_failures"] == 1
        assert stats["current_backoff_s"] == 60.0


# ────────────────────────────────────────────────────
# CircuitBreaker
# ────────────────────────────────────────────────────


class TestCircuitBreaker:
    def test_initial_closed(self, tmp_path):
        cb = CircuitBreaker(tmp_path / ".circuit")
        assert cb.is_open() is False

    def test_opens_after_max_failures(self, tmp_path):
        cb = CircuitBreaker(tmp_path / ".circuit", max_failures=3)
        for _ in range(3):
            cb.record_failure()
        assert cb.is_open() is True

    def test_stays_closed_below_threshold(self, tmp_path):
        cb = CircuitBreaker(tmp_path / ".circuit", max_failures=5)
        for _ in range(4):
            cb.record_failure()
        assert cb.is_open() is False

    def test_success_resets(self, tmp_path):
        marker = tmp_path / ".circuit"
        cb = CircuitBreaker(marker, max_failures=2)
        cb.record_failure()
        cb.record_failure()
        assert cb.is_open() is True
        cb.record_success()
        assert cb.is_open() is False
        assert not marker.exists()

    def test_auto_reset_after_timeout(self, tmp_path):
        marker = tmp_path / ".circuit"
        cb = CircuitBreaker(marker, max_failures=2, reset_after=0.05)
        cb.record_failure()
        cb.record_failure()
        assert cb.is_open() is True
        time.sleep(0.06)
        assert cb.is_open() is False
        assert not marker.exists()

    def test_marker_file_persists(self, tmp_path):
        marker = tmp_path / ".circuit"
        cb1 = CircuitBreaker(marker, max_failures=2)
        cb1.record_failure()
        cb1.record_failure()

        # 新进程读同一个文件
        cb2 = CircuitBreaker(marker, max_failures=2)
        assert cb2.is_open() is True

    def test_corrupted_marker_handled(self, tmp_path):
        marker = tmp_path / ".circuit"
        marker.write_text("not json")
        cb = CircuitBreaker(marker, max_failures=2)
        assert cb.is_open() is False

    def test_missing_parent_dir(self, tmp_path):
        marker = tmp_path / "subdir" / ".circuit"
        cb = CircuitBreaker(marker, max_failures=2)
        cb.record_failure()
        assert marker.exists()


# ────────────────────────────────────────────────────
# FileStateCircuitBreaker (#237)
# ────────────────────────────────────────────────────


class TestFileStateCircuitBreaker:
    def test_initial_closed(self, tmp_path):
        cb = FileStateCircuitBreaker("test", tmp_path, threshold=3)
        assert cb.is_open() is False
        assert cb.is_available() is True

    def test_opens_at_threshold(self, tmp_path):
        cb = FileStateCircuitBreaker("test", tmp_path, threshold=3)
        cb.record_failure()
        cb.record_failure()
        assert cb.is_open() is False
        cb.record_failure()  # 3rd = threshold
        assert cb.is_open() is True

    def test_exponential_backoff(self, tmp_path):
        t = 1000.0
        cb = FileStateCircuitBreaker(
            "test", tmp_path, threshold=3,
            cooldown_seconds=100.0, max_cooldown_seconds=1000.0,
            time_func=lambda: t,
        )
        for _ in range(3):
            cb.record_failure()
        # failures=3: 100 * 2^0 = 100s
        assert cb._current_cooldown(3) == 100.0
        # failures=4: 100 * 2^1 = 200s
        assert cb._current_cooldown(4) == 200.0
        # failures=5: 100 * 2^2 = 400s
        assert cb._current_cooldown(5) == 400.0
        # capped at max
        assert cb._current_cooldown(10) == 1000.0

    def test_half_open_recovery(self, tmp_path):
        t = [1000.0]
        cb = FileStateCircuitBreaker(
            "test", tmp_path, threshold=2,
            cooldown_seconds=10.0, max_cooldown_seconds=100.0,
            time_func=lambda: t[0],
        )
        cb.record_failure()
        cb.record_failure()
        assert cb.is_open() is True

        # 冷却期过后，进入 half-open
        t[0] = 1011.0  # 超过 10s cooldown
        assert cb.is_open() is False  # half-open 允许探测

        # 探测成功 → closed
        cb.record_success()
        assert cb.is_open() is False
        assert cb.stats["failures"] == 0

    def test_half_open_probe_failure(self, tmp_path):
        t = [1000.0]
        cb = FileStateCircuitBreaker(
            "test", tmp_path, threshold=2,
            cooldown_seconds=10.0, max_cooldown_seconds=100.0,
            time_func=lambda: t[0],
        )
        cb.record_failure()
        cb.record_failure()

        # 冷却期过
        t[0] = 1011.0
        assert cb.is_open() is False  # half-open

        # 探测失败 → open，退避更长
        cb.record_failure()
        assert cb.is_open() is True
        # failures=3: cooldown = 10 * 2^(3-2) = 20s
        assert cb._current_cooldown(3) == 20.0

    def test_cross_process_persistence(self, tmp_path):
        """模拟跨 hook 进程：不同实例共享文件状态"""
        cb1 = FileStateCircuitBreaker("test", tmp_path, threshold=2)
        cb1.record_failure()
        cb1.record_failure()

        # 新"进程"读同一目录
        cb2 = FileStateCircuitBreaker("test", tmp_path, threshold=2)
        assert cb2.is_open() is True
        assert cb2.stats["failures"] == 2

    def test_success_resets_from_any_state(self, tmp_path):
        cb = FileStateCircuitBreaker("test", tmp_path, threshold=2)
        cb.record_failure()
        cb.record_failure()
        cb.record_failure()
        assert cb.is_open() is True

        cb.record_success()
        assert cb.is_open() is False
        assert cb.stats["failures"] == 0

    def test_stats(self, tmp_path):
        cb = FileStateCircuitBreaker("test", tmp_path, threshold=3)
        stats = cb.stats
        assert stats["failures"] == 0
        assert stats["threshold"] == 3
        assert stats["is_open"] is False
        assert stats["half_open"] is False

        for _ in range(3):
            cb.record_failure()
        stats = cb.stats
        assert stats["failures"] == 3
        assert stats["is_open"] is True
        assert stats["cooldown_seconds"] > 0

    def test_corrupted_state_file(self, tmp_path):
        state_file = tmp_path / "test.cb.json"
        state_file.write_text("not json")
        cb = FileStateCircuitBreaker("test", tmp_path, threshold=2)
        # 损坏文件应该返回默认（closed）
        assert cb.is_open() is False

    def test_missing_state_dir(self, tmp_path):
        cb = FileStateCircuitBreaker(
            "test", tmp_path / "nested" / "dir", threshold=2,
        )
        cb.record_failure()
        # 应该自动创建目录
        assert (tmp_path / "nested" / "dir" / "test.cb.json").exists()

    def test_no_write_on_no_change(self, tmp_path):
        """record_success on already-clean state shouldn't write"""
        cb = FileStateCircuitBreaker("test", tmp_path, threshold=2)
        state_file = tmp_path / "test.cb.json"
        cb.record_success()
        # 没有失败过 → 不需要写文件
        assert not state_file.exists()


# ────────────────────────────────────────────────────
# safe_db_write
# ────────────────────────────────────────────────────


class TestSafeDbWrite:
    def _make_db(self, tmp_path):
        db_path = tmp_path / "test.db"
        conn = sqlite3.connect(str(db_path))
        conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)")
        conn.commit()
        return conn, db_path

    def test_normal_write(self, tmp_path):
        conn, _ = self._make_db(tmp_path)
        rowid = safe_db_write(conn, "INSERT INTO t (val) VALUES (?)", ["hello"])
        assert rowid is not None
        assert rowid > 0
        row = conn.execute("SELECT val FROM t WHERE id = ?", [rowid]).fetchone()
        assert row[0] == "hello"
        conn.close()

    def test_locked_returns_none(self, tmp_path):
        """真实锁场景：conn2 持有排他锁，conn1 写入超时"""
        conn1, db_path = self._make_db(tmp_path)
        conn1.close()
        # 极短 timeout 确保快速失败
        conn1 = sqlite3.connect(str(db_path), timeout=0.01)
        conn2 = sqlite3.connect(str(db_path))
        # conn2 开始写事务，持有排他锁
        conn2.execute("BEGIN EXCLUSIVE")
        try:
            result = safe_db_write(conn1, "INSERT INTO t (val) VALUES (?)", ["x"])
            assert result is None
        finally:
            conn2.rollback()
            conn1.close()
            conn2.close()

    def test_malformed_returns_none(self, tmp_path):
        """真实损坏场景：覆写 DB 文件头"""
        _, db_path = self._make_db(tmp_path)
        # 损坏数据库文件
        with open(db_path, "r+b") as f:
            f.write(b"CORRUPTED" * 10)
        conn = sqlite3.connect(str(db_path))
        result = safe_db_write(conn, "INSERT INTO t (val) VALUES (?)", ["x"])
        assert result is None
        conn.close()

    def test_other_error_propagates(self, tmp_path):
        conn, _ = self._make_db(tmp_path)
        with pytest.raises(sqlite3.OperationalError):
            safe_db_write(conn, "INVALID SQL SYNTAX")
        conn.close()


# ────────────────────────────────────────────────────
# safe_db_transaction
# ────────────────────────────────────────────────────


class TestSafeDbTransaction:
    def _make_db(self, tmp_path):
        db_path = tmp_path / "test.db"
        conn = sqlite3.connect(str(db_path))
        conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)")
        conn.commit()
        return conn, db_path

    def test_normal_transaction(self, tmp_path):
        conn, _ = self._make_db(tmp_path)

        def ops(c):
            c.execute("INSERT INTO t (val) VALUES (?)", ["a"])
            c.execute("INSERT INTO t (val) VALUES (?)", ["b"])
            return 42

        ok, result = safe_db_transaction(conn, ops)
        assert ok is True
        assert result == 42
        rows = conn.execute("SELECT COUNT(*) FROM t").fetchone()[0]
        assert rows == 2
        conn.close()

    def test_rollback_on_error(self, tmp_path):
        conn, _ = self._make_db(tmp_path)

        def ops(c):
            c.execute("INSERT INTO t (val) VALUES (?)", ["a"])
            c.execute("INVALID SQL")  # 这会导致回滚

        with pytest.raises(sqlite3.OperationalError):
            safe_db_transaction(conn, ops)

        # 由于回滚，a 不应该在表中
        rows = conn.execute("SELECT COUNT(*) FROM t").fetchone()[0]
        assert rows == 0
        conn.close()

    def test_locked_returns_false(self, tmp_path):
        """真实锁场景"""
        conn, db_path = self._make_db(tmp_path)
        conn.close()
        conn = sqlite3.connect(str(db_path), timeout=0.01)
        conn2 = sqlite3.connect(str(db_path))
        conn2.execute("BEGIN EXCLUSIVE")
        try:
            def ops(c):
                c.execute("INSERT INTO t (val) VALUES (?)", ["x"])

            ok, result = safe_db_transaction(conn, ops)
            assert ok is False
            assert result is None
        finally:
            conn2.rollback()
            conn.close()
            conn2.close()

    def test_malformed_returns_false(self, tmp_path):
        """真实损坏场景"""
        _, db_path = self._make_db(tmp_path)
        with open(db_path, "r+b") as f:
            f.write(b"CORRUPTED" * 10)
        conn = sqlite3.connect(str(db_path))

        def ops(c):
            c.execute("INSERT INTO t (val) VALUES (?)", ["x"])

        ok, result = safe_db_transaction(conn, ops)
        assert ok is False
        conn.close()


# ────────────────────────────────────────────────────
# mcp_error_boundary
# ────────────────────────────────────────────────────


class TestMcpErrorBoundary:
    def test_normal_passthrough(self):
        @mcp_error_boundary
        async def my_tool(x):
            return {"result": x * 2}

        result = asyncio.run(my_tool(5))
        assert result == {"result": 10}

    def test_db_error_caught(self):
        @mcp_error_boundary
        async def my_tool():
            raise sqlite3.DatabaseError("database disk image is malformed")

        result = asyncio.run(my_tool())
        assert "error" in result
        assert "数据库异常" in result["error"]
        assert "malformed" in result["detail"]

    def test_generic_error_caught(self):
        @mcp_error_boundary
        async def my_tool():
            raise ValueError("bad input")

        result = asyncio.run(my_tool())
        assert "error" in result
        assert "内部错误" in result["error"]
        assert "bad input" in result["detail"]

    def test_preserves_function_name(self):
        @mcp_error_boundary
        async def docs_search():
            pass

        assert docs_search.__name__ == "docs_search"

    def test_keyboard_interrupt_propagates(self):
        @mcp_error_boundary
        async def my_tool():
            raise KeyboardInterrupt()

        with pytest.raises(KeyboardInterrupt):
            asyncio.run(my_tool())
