"""素材片段使用记录追踪 + 受控复用机制测试（asset_segment_tracker）.

覆盖：
- get_used_segments 聚合 metadata 中持久化的区间
- record_used_segments 追加新记录（use_count=1，保留原有 metadata 字段）
- record_used_segments 复用同一区间时累加 use_count / 刷新 last_used_at
- remove_used_segment 匹配删除（tolerance + plan_id，旧数据按时间匹配）
- reset_used_segments 清空（其他字段不动）
- find_reusable_range：选最久未用且 use_count<3 的区间；全部达上限返回 None
- make_reuse_callback：返回复用区间、累加 reused_tracker、DB 异常返回 None
- _calc_random_start_time：100 次避不开时调用复用回调返回历史区间（不再清空历史）
"""

from __future__ import annotations

import json
import os
import sys
from pathlib import Path

os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")

sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))

import pytest
from app.services import asset_segment_tracker as ast
from app.services.asset_segment_tracker import (
    MAX_RANGE_USE_COUNT,
    REUSE_RATIO_LIMIT,
    SEGMENT_EDGE_GAP,
    find_reusable_range,
    get_used_segments,
    make_reuse_callback,
    record_used_segments,
    remove_used_segment,
    reset_used_segments,
)

from packages.domain.plan_generator_utils import _calc_random_start_time


class FakeModel:
    """模拟 AssetModel：id + classification_result（JSON Text）+ updated_at。"""

    def __init__(self, asset_id: str, meta: dict | None = None):
        self.id = asset_id
        self.classification_result = json.dumps(meta, ensure_ascii=False) if meta else None
        self.updated_at = None

    def meta(self) -> dict:
        return json.loads(self.classification_result) if self.classification_result else {}


class _InExpr:
    def __init__(self, ids, models):
        self._ids = ids
        self._models = models

    def all(self):
        return [self._models[i] for i in self._ids if i in self._models]


class _EqExpr:
    def __init__(self, target_id, models):
        self._target_id = target_id
        self._models = models

    def with_for_update(self):
        # 模拟 SQLAlchemy Query.with_for_update() 链式返回自身
        return self

    def first(self):
        return self._models.get(self._target_id)


class FakeSession:
    """模拟 db：db.query(Model).filter(Model.id.in_(ids)).all() / .filter(Model.id == id).first()。"""

    class _Col:
        def __init__(self, models):
            self._models = models

        def in_(self, ids):
            return _InExpr(list(ids), self._models)

        def __eq__(self, other):
            return _EqExpr(other, self._models)

    def __init__(self, models: dict[str, FakeModel]):
        self._models = models
        self.commits = 0

    def query(self, _model):
        col = self._Col(self._models)

        class _Q:
            def filter(self_inner, expr):
                return expr

        q = _Q()
        _model.id = col
        return q

    def commit(self):
        self.commits += 1


@pytest.fixture
def patched_model(monkeypatch):
    monkeypatch.setattr(ast, "AssetModel", FakeModel)


def _db(models):
    return FakeSession(models)


def _ranges(db, aid="a1"):
    model = db._models[aid]
    return json.loads(model.classification_result)["used_time_ranges"]


# ── 配置常量 ──────────────────────────────────────────────────────────────────


def test_config_constants():
    assert MAX_RANGE_USE_COUNT == 2
    assert REUSE_RATIO_LIMIT == 0.10
    assert SEGMENT_EDGE_GAP == 1.5


# ── get_used_segments ─────────────────────────────────────────────────────────


def test_get_used_segments_aggregates_ranges(patched_model):
    models = {
        "a1": FakeModel(
            "a1",
            {
                "used_time_ranges": [
                    {"start": 1.0, "end": 5.0, "plan_id": "p1", "use_count": 2},
                    {"start": 9.0, "end": 12.0, "plan_id": "p2"},
                ]
            },
        ),
        "a2": FakeModel("a2", {"other": 1}),
        "a3": FakeModel("a3"),
    }
    db = _db(models)
    assert get_used_segments(db, ["a1", "a2", "a3", "missing"]) == {"a1": [(1.0, 5.0), (9.0, 12.0)]}


def test_get_used_segments_empty(patched_model):
    assert get_used_segments(_db({}), []) == {}


# ── record_used_segments ──────────────────────────────────────────────────────


def test_record_appends_new_range_with_use_count_one(patched_model):
    models = {"a1": FakeModel("a1", {"generation_use_count": 48, "review_status": "pending_review"})}
    db = _db(models)
    record_used_segments(db, "a1", 12.5, 20.3, "plan-x")
    meta = json.loads(models["a1"].classification_result)
    assert meta["generation_use_count"] == 48
    assert meta["review_status"] == "pending_review"
    ranges = meta["used_time_ranges"]
    assert len(ranges) == 1
    assert ranges[0]["start"] == 12.5 and ranges[0]["end"] == 20.3
    assert ranges[0]["plan_id"] == "plan-x"
    assert ranges[0]["use_count"] == 1
    assert "created_at" in ranges[0] and "last_used_at" in ranges[0]
    assert db.commits == 0  # 不自行 commit


def test_record_reuse_same_range_increments_use_count(patched_model):
    """新片段与历史区间高度重叠（复用）→ 累加 use_count，不新增记录。"""
    models = {
        "a1": FakeModel(
            "a1",
            {
                "used_time_ranges": [
                    {
                        "start": 10.0,
                        "end": 20.0,
                        "plan_id": "p1",
                        "use_count": 1,
                        "created_at": "2026-01-01T00:00:00+00:00",
                        "last_used_at": "2026-01-01T00:00:00+00:00",
                    },
                ]
            },
        )
    }
    db = _db(models)
    # 同一起点复用（find_reusable_range 返回的就是历史区间起点）
    record_used_segments(db, "a1", 10.0, 20.0, "p2")
    ranges = _ranges(db)
    assert len(ranges) == 1
    assert ranges[0]["use_count"] == 2
    assert ranges[0]["last_used_at"] != "2026-01-01T00:00:00+00:00"


def test_record_distinct_range_appends(patched_model):
    models = {
        "a1": FakeModel(
            "a1",
            {
                "used_time_ranges": [
                    {"start": 10.0, "end": 20.0, "plan_id": "p1", "use_count": 1},
                ]
            },
        )
    }
    db = _db(models)
    record_used_segments(db, "a1", 25.0, 35.0, "p2")
    ranges = _ranges(db)
    assert len(ranges) == 2
    assert ranges[1]["use_count"] == 1


def test_record_missing_asset_no_raise(patched_model):
    db = _db({})
    record_used_segments(db, "ghost", 1.0, 2.0, "p")  # 不抛异常


# ── remove_used_segment ───────────────────────────────────────────────────────


def test_remove_matching_range(patched_model):
    models = {
        "a1": FakeModel(
            "a1",
            {
                "used_time_ranges": [
                    {"start": 1.0, "end": 5.0, "plan_id": "p1"},
                    {"start": 9.0, "end": 12.0, "plan_id": "p2"},
                ]
            },
        )
    }
    db = _db(models)
    assert remove_used_segment(db, "a1", 1.0, 5.0, plan_id="p1") is True
    assert len(_ranges(db)) == 1
    assert _ranges(db)[0]["start"] == 9.0


def test_remove_plan_mismatch_keeps_range(patched_model):
    models = {"a1": FakeModel("a1", {"used_time_ranges": [{"start": 1.0, "end": 5.0, "plan_id": "p1"}]})}
    db = _db(models)
    assert remove_used_segment(db, "a1", 1.0, 5.0, plan_id="other") is False
    assert len(_ranges(db)) == 1


def test_remove_legacy_range_without_plan_id(patched_model):
    """旧数据记录缺 plan_id → 按时间匹配可删除。"""
    models = {
        "a1": FakeModel(
            "a1",
            {
                "used_time_ranges": [
                    {"start": 2.0, "end": 12.0, "created_at": "2026-01-01T00:00:00"},
                ]
            },
        )
    }
    db = _db(models)
    assert remove_used_segment(db, "a1", 2.0, 12.0, plan_id="plan-new") is True
    assert _ranges(db) == []


# ── reset_used_segments（仅运维/测试）─────────────────────────────────────────


def test_reset_clears_ranges_keeps_other_fields(patched_model):
    models = {
        "a1": FakeModel(
            "a1",
            {
                "generation_use_count": 3,
                "used_time_ranges": [
                    {"start": 1.0, "end": 5.0},
                ],
            },
        )
    }
    db = _db(models)
    reset_used_segments(db, "a1")
    meta = json.loads(models["a1"].classification_result)
    assert meta["used_time_ranges"] == []
    assert meta["generation_use_count"] == 3


# ── find_reusable_range：受控复用选择 ─────────────────────────────────────────


def test_find_reusable_prefers_oldest_unused(patched_model):
    """选 last_used_at 最老、use_count 未达上限的区间；能容纳 clip_duration。"""
    models = {
        "a1": FakeModel(
            "a1",
            {
                "used_time_ranges": [
                    {"start": 0.0, "end": 8.0, "use_count": 1, "last_used_at": "2026-08-01T00:00:00+00:00"},
                    {
                        "start": 10.0,
                        "end": 20.0,
                        "use_count": 1,
                        "last_used_at": "2026-01-01T00:00:00+00:00",
                    },  # 最久未用
                ]
            },
        )
    }
    db = _db(models)
    result = find_reusable_range(db, "a1", clip_duration=5.0, asset_total=30.0)
    assert result is not None
    start, end = result
    assert start == 10.0 and end == 15.0


def test_find_reusable_excludes_max_use_count(patched_model):
    """use_count 达到上限（2）的区间不再参与复用；全部达上限返回 None。"""
    models = {
        "a1": FakeModel(
            "a1",
            {
                "used_time_ranges": [
                    {"start": 0.0, "end": 10.0, "use_count": 2, "last_used_at": "2026-01-01T00:00:00"},
                ]
            },
        )
    }
    db = _db(models)
    assert find_reusable_range(db, "a1", 5.0, 30.0) is None


def test_find_reusable_third_use_rejected(patched_model):
    """同区间复用第 3 次被拒绝：use_count=1 的可复用，use_count=2 的不可复用。"""
    models = {
        "a1": FakeModel(
            "a1",
            {
                "used_time_ranges": [
                    {"start": 0.0, "end": 10.0, "use_count": 1, "last_used_at": "2026-03-01T00:00:00"},
                    {"start": 10.0, "end": 20.0, "use_count": 2, "last_used_at": "2026-01-01T00:00:00"},
                ]
            },
        )
    }
    db = _db(models)
    result = find_reusable_range(db, "a1", 5.0, 30.0)
    # 只能选 use_count=1 的区间（start=0），不能选 use_count=2 的（虽然它更老）
    assert result is not None and result[0] == 0.0


def test_find_reusable_clamps_to_asset_bounds(patched_model):
    """历史区间起点 + clip_duration 会越素材末尾时，起点钳制到 max_start。"""
    models = {
        "a1": FakeModel(
            "a1",
            {
                "used_time_ranges": [
                    {"start": 25.0, "end": 30.0, "use_count": 1, "last_used_at": "2026-01-01T00:00:00"},
                ]
            },
        )
    }
    db = _db(models)
    result = find_reusable_range(db, "a1", clip_duration=10.0, asset_total=30.0)
    assert result is not None
    start, end = result
    assert end <= 30.0 + 1e-6 and start >= 0.0


def test_find_reusable_no_ranges_returns_none(patched_model):
    models = {"a1": FakeModel("a1", {"other": 1})}
    db = _db(models)
    assert find_reusable_range(db, "a1", 5.0, 30.0) is None


# ── make_reuse_callback ───────────────────────────────────────────────────────


def test_reuse_callback_returns_range_and_tracks_duration(patched_model):
    models = {
        "a1": FakeModel(
            "a1",
            {
                "used_time_ranges": [
                    {"start": 10.0, "end": 20.0, "use_count": 1, "last_used_at": "2026-01-01T00:00:00"},
                ]
            },
        )
    }
    db = _db(models)
    reused: dict[str, float] = {}
    cb = make_reuse_callback(db, {"a1": 30.0}, reused)
    result = cb("a1", 8.0)
    assert result is not None and result[0] == 10.0
    assert reused["a1"] == 8.0  # 复用时长累加


def test_reuse_callback_db_error_returns_none(patched_model):
    class BoomSession:
        def query(self, _m):
            raise RuntimeError("db down")

    reused: dict[str, float] = {}
    cb = make_reuse_callback(BoomSession(), {"a1": 30.0}, reused)
    assert cb("a1", 8.0) is None  # 异常被吞，返回 None
    assert reused == {}


# ── _calc_random_start_time 与受控回调集成 ────────────────────────────────────


def test_calc_random_start_uses_reuse_callback_when_exhausted(monkeypatch):
    """素材区间被占满、100 次随机找不到空位时，调用复用回调返回历史区间。"""
    import packages.domain.plan_generator_utils as pgu

    monkeypatch.setattr(pgu.random, "uniform", lambda a, b: 0.5)  # 固定候选点必撞区间

    durations = {"a1": 30.0}
    used = {"a1": [(0.0, 30.0)]}  # 全占满
    calls = []

    def reuse_cb(asset_id, clip_duration):
        calls.append((asset_id, clip_duration))
        return (10.0, 18.0)

    result = _calc_random_start_time("a1", 8.0, durations, used, on_exhausted=reuse_cb)
    assert calls == [("a1", 8.0)]
    assert result == 10.0


def test_calc_random_start_reuse_callback_none_returns_none(monkeypatch):
    """复用回调返回 None（区间全部达上限/复用占比超闸门）→ calc 返回 None。

    新机制下不做末尾/0.0 重叠降级（那会把片段放回已用过的画面），
    由调用方轮询下一个素材或报 400；历史记录不被清空。
    """
    import packages.domain.plan_generator_utils as pgu

    monkeypatch.setattr(pgu.random, "uniform", lambda a, b: 0.5)

    durations = {"a1": 30.0}
    used = {"a1": [(0.0, 30.0)]}
    used_before = list(used["a1"])
    result = _calc_random_start_time("a1", 8.0, durations, used, on_exhausted=lambda aid, d: None)
    assert result is None
    assert used["a1"] == used_before  # 历史记录未被清空


def test_calc_random_start_no_callback_backward_compatible(monkeypatch):
    """不传 on_exhausted 时行为与旧版兼容（100 次失败走降级）。"""
    import packages.domain.plan_generator_utils as pgu

    monkeypatch.setattr(pgu.random, "uniform", lambda a, b: 0.5)
    result = _calc_random_start_time("a1", 8.0, {"a1": 30.0}, {"a1": [(0.0, 30.0)]})
    assert result is not None
