# -*- coding: utf-8 -*-
"""CL CronTaskService durable 定时任务测试（Phase 8 Tier 2）

借鉴 zhikuncode CronTaskService.java：
- 内存 + durable 文件持久化（~/.abyss/scheduled_tasks.json）
- 原子写（.tmp + os.replace）
- 上限 50 条
- 30 天过期自动清理
"""
import json
import os
import time
import threading
from pathlib import Path
from unittest.mock import patch, MagicMock

import pytest

from abyss.tools.cron_service import CronTaskService, DurableCronTask


class TestDurableCronTask:
    """数据类测试"""

    def test_default_fields(self):
        task = DurableCronTask(
            id="t1",
            interval_seconds=60,
            command="/save",
            next_run=time.time() + 60,
            created_at=time.time(),
        )
        assert task.last_run is None
        assert task.last_result is None

    def test_to_dict_and_from_dict_roundtrip(self):
        task = DurableCronTask(
            id="t1",
            interval_seconds=60,
            command="/save",
            next_run=1000.0,
            created_at=900.0,
            last_run=950.0,
            last_result="ok",
        )
        d = task.to_dict()
        restored = DurableCronTask.from_dict(d)
        assert restored.id == task.id
        assert restored.interval_seconds == task.interval_seconds
        assert restored.command == task.command
        assert restored.next_run == task.next_run
        assert restored.created_at == task.created_at
        assert restored.last_run == task.last_run
        assert restored.last_result == task.last_result


class TestCronTaskServiceCreate:
    """create 测试"""

    def test_create_assigns_id(self, tmp_path):
        svc = CronTaskService(persist_path=str(tmp_path / "tasks.json"))
        task_id = svc.create(interval=60, command="/save")
        assert task_id is not None
        assert len(task_id) > 0

    def test_create_assigns_next_run(self, tmp_path):
        svc = CronTaskService(persist_path=str(tmp_path / "tasks.json"))
        before = time.time()
        task_id = svc.create(interval=60, command="/save")
        after = time.time()
        tasks = svc.list()
        assert len(tasks) == 1
        assert tasks[0].next_run >= before + 60
        assert tasks[0].next_run <= after + 60 + 1

    def test_create_persists_to_file(self, tmp_path):
        path = tmp_path / "tasks.json"
        svc = CronTaskService(persist_path=str(path))
        svc.create(interval=60, command="/save")
        assert path.exists()
        data = json.loads(path.read_text(encoding="utf-8"))
        assert len(data) == 1
        assert data[0]["command"] == "/save"

    def test_max_50_tasks_enforced(self, tmp_path):
        """超过上限自动淘汰最老的"""
        svc = CronTaskService(persist_path=str(tmp_path / "tasks.json"), max_tasks=50)
        for i in range(55):
            svc.create(interval=60, command=f"/cmd{i}")
        tasks = svc.list()
        assert len(tasks) == 50
        # 应淘汰最老的（cmd0 到 cmd4）
        commands = [t.command for t in tasks]
        assert "/cmd0" not in commands
        assert "/cmd4" not in commands
        assert "/cmd5" in commands
        assert "/cmd54" in commands


class TestCronTaskServiceListDelete:
    """list / delete 测试"""

    def test_list_returns_all(self, tmp_path):
        svc = CronTaskService(persist_path=str(tmp_path / "tasks.json"))
        svc.create(interval=60, command="/a")
        svc.create(interval=120, command="/b")
        tasks = svc.list()
        assert len(tasks) == 2

    def test_list_empty_when_no_tasks(self, tmp_path):
        svc = CronTaskService(persist_path=str(tmp_path / "tasks.json"))
        assert svc.list() == []

    def test_delete_removes_task(self, tmp_path):
        svc = CronTaskService(persist_path=str(tmp_path / "tasks.json"))
        task_id = svc.create(interval=60, command="/a")
        assert svc.delete(task_id) is True
        assert svc.list() == []

    def test_delete_unknown_returns_false(self, tmp_path):
        svc = CronTaskService(persist_path=str(tmp_path / "tasks.json"))
        assert svc.delete("nonexistent") is False

    def test_delete_persists(self, tmp_path):
        path = tmp_path / "tasks.json"
        svc = CronTaskService(persist_path=str(path))
        tid1 = svc.create(interval=60, command="/a")
        svc.create(interval=60, command="/b")
        svc.delete(tid1)
        data = json.loads(path.read_text(encoding="utf-8"))
        assert len(data) == 1
        assert data[0]["command"] == "/b"


class TestCronTaskServiceTriggerDue:
    """trigger_due 测试"""

    def test_trigger_due_executes_expired_tasks(self, tmp_path):
        svc = CronTaskService(persist_path=str(tmp_path / "tasks.json"))
        # 创建一个立即到期的任务
        task_id = svc.create(interval=1, command="/due-now")
        # 手动设 next_run 为过去
        with svc._lock:
            for t in svc._tasks:
                if t.id == task_id:
                    t.next_run = time.time() - 1
        triggered = svc.trigger_due()
        assert len(triggered) == 1
        assert triggered[0] == "/due-now"
        # last_run 更新
        tasks = svc.list()
        assert tasks[0].last_run is not None
        assert tasks[0].last_result is not None or tasks[0].last_result == ""

    def test_trigger_due_skips_future_tasks(self, tmp_path):
        svc = CronTaskService(persist_path=str(tmp_path / "tasks.json"))
        svc.create(interval=3600, command="/future")  # 1 小时后
        triggered = svc.trigger_due()
        assert triggered == []

    def test_trigger_due_updates_next_run(self, tmp_path):
        """触发后 next_run 应更新为下次时间"""
        svc = CronTaskService(persist_path=str(tmp_path / "tasks.json"))
        task_id = svc.create(interval=60, command="/every-min")
        with svc._lock:
            for t in svc._tasks:
                if t.id == task_id:
                    t.next_run = time.time() - 1
                    old_interval = t.interval_seconds
        svc.trigger_due()
        tasks = svc.list()
        # next_run 应大于现在
        assert tasks[0].next_run >= time.time() + old_interval - 5

    def test_trigger_due_returns_commands(self, tmp_path):
        svc = CronTaskService(persist_path=str(tmp_path / "tasks.json"))
        # 创建两个到期任务
        tid1 = svc.create(interval=1, command="/cmd-a")
        tid2 = svc.create(interval=1, command="/cmd-b")
        with svc._lock:
            for t in svc._tasks:
                t.next_run = time.time() - 1
        triggered = svc.trigger_due()
        assert "/cmd-a" in triggered
        assert "/cmd-b" in triggered


class TestCronTaskServiceExpire:
    """过期清理测试"""

    def test_30_day_expiry_purges_old_tasks(self, tmp_path):
        svc = CronTaskService(persist_path=str(tmp_path / "tasks.json"), expire_days=30)
        task_id = svc.create(interval=60, command="/old")
        # 手动设 created_at 为 31 天前
        with svc._lock:
            for t in svc._tasks:
                if t.id == task_id:
                    t.created_at = time.time() - 31 * 86400
        svc._purge_expired()
        assert svc.list() == []

    def test_recent_tasks_not_purged(self, tmp_path):
        svc = CronTaskService(persist_path=str(tmp_path / "tasks.json"), expire_days=30)
        svc.create(interval=60, command="/recent")
        svc._purge_expired()
        assert len(svc.list()) == 1


class TestCronTaskServicePersistence:
    """持久化测试"""

    def test_persist_and_load_roundtrip(self, tmp_path):
        path = tmp_path / "tasks.json"
        svc1 = CronTaskService(persist_path=str(path))
        svc1.create(interval=60, command="/persisted")
        # 新实例加载
        svc2 = CronTaskService(persist_path=str(path))
        tasks = svc2.list()
        assert len(tasks) == 1
        assert tasks[0].command == "/persisted"

    def test_atomic_write_uses_tmp_file(self, tmp_path):
        path = tmp_path / "tasks.json"
        svc = CronTaskService(persist_path=str(path))
        svc.create(interval=60, command="/atomic")
        # .tmp 文件不应残留
        tmp_path_file = Path(str(path) + ".tmp")
        assert not tmp_path_file.exists()

    def test_corrupted_file_handled_gracefully(self, tmp_path):
        """损坏文件不崩溃，从空开始"""
        path = tmp_path / "tasks.json"
        path.write_text("not valid json {{{", encoding="utf-8")
        # 不抛异常
        svc = CronTaskService(persist_path=str(path))
        assert svc.list() == []

    def test_missing_file_handled(self, tmp_path):
        path = tmp_path / "nonexistent.json"
        svc = CronTaskService(persist_path=str(path))
        assert svc.list() == []


class TestCronTaskServiceConcurrency:
    """并发测试"""

    def test_concurrent_create_thread_safe(self, tmp_path):
        svc = CronTaskService(persist_path=str(tmp_path / "tasks.json"))

        def create_one():
            svc.create(interval=60, command=f"/cmd-{threading.get_ident()}")

        threads = [threading.Thread(target=create_one) for _ in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()
        assert len(svc.list()) == 10

    def test_concurrent_trigger_due_thread_safe(self, tmp_path):
        svc = CronTaskService(persist_path=str(tmp_path / "tasks.json"))
        # 创建多个到期任务
        for i in range(5):
            tid = svc.create(interval=1, command=f"/cmd-{i}")
            with svc._lock:
                for t in svc._tasks:
                    if t.id == tid:
                        t.next_run = time.time() - 1

        def trigger():
            svc.trigger_due()

        threads = [threading.Thread(target=trigger) for _ in range(3)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()
        # 不崩溃即可（trigger_due 应有锁保护）
