from pathlib import Path

import pytest

from app.chain.transfer import TransferChain
from app.helper.format import EpisodeFormatRuleHelper, FormatParser, _AutoRecommendSample
from app.schemas import EpisodeFormatRule, FileItem


def _make_file(name: str, size: int = 150 * 1024 * 1024) -> FileItem:
    suffix = Path(name).suffix.lstrip(".")
    return FileItem(
        storage="local",
        path=f"/downloads/{name}",
        type="file",
        name=name,
        extension=suffix,
        size=size,
    )


@pytest.fixture(autouse=True)
def _patch_media_exts(monkeypatch):
    monkeypatch.setattr(
        "app.helper.format.settings.RMT_MEDIAEXT",
        [".mkv", ".mp4"],
    )


def test_rule_recommend_supports_range_episode_validation():
    helper = EpisodeFormatRuleHelper()
    rule = EpisodeFormatRule(
        name="区间规则",
        pattern=r"Show\.S01E(?<ep>\d{2}-E\d{2})\.mkv",
    )
    sample = _make_file("Show.S01E01-E02.mkv")

    state, errmsg, data = helper.recommend([rule], [sample])

    assert state is True
    assert errmsg == ""
    assert data["episode_format"] == "Show.S01E{ep}.mkv"


def test_episode_matches_rejects_missing_range_end():
    helper = EpisodeFormatRuleHelper()

    assert helper._episode_matches(1, None, "01-E02") is False


def test_locate_episode_ignores_version_suffix_digits():
    helper = EpisodeFormatRuleHelper()
    file_name = "Show - 01 v2.mkv"

    start, end = helper._locate_episode(file_name, "01")

    assert file_name[start:end] == "01"
    assert file_name[start - 1] == " "


def test_locate_episode_supports_hash_prefix():
    helper = EpisodeFormatRuleHelper()
    file_name = "[AI-Raws] 不滅のあなたへ #01 (BD HEVC 1920x1080 yuv444p10le FLAC)[DE0EC3BA].mkv"

    start, end = helper._locate_episode(file_name, "01")

    assert file_name[start:end] == "01"
    assert file_name[start - 1] == "#"


def test_auto_recommend_returns_low_confidence_for_single_sample():
    helper = EpisodeFormatRuleHelper()
    sample = _make_file("[Seed-Raws] Tari Tari - 01 (BD 1280x720 AVC AAC).mp4")

    state, errmsg, data = helper.recommend([], [sample])

    assert state is True
    assert errmsg == ""
    assert data["confidence"] == "low"
    assert data["sample_count"] == 1
    assert data["majority_count"] == 1
    assert data["reason"] == "single_sample_only"
    assert "single_sample_only" in data["reasons"]
    assert "单文件" in data["message"]


def test_auto_recommend_relaxes_size_filter_for_small_media():
    helper = EpisodeFormatRuleHelper()
    samples = [
        _make_file("Show.S01E01.mkv", size=40 * 1024 * 1024),
        _make_file("Show.S01E02.mkv", size=42 * 1024 * 1024),
    ]

    state, errmsg, data = helper.recommend([], samples)

    assert state is True
    assert errmsg == ""
    assert data["size_filter_relaxed"] is True
    assert data["confidence"] == "low"
    assert "small_files_fallback" in data["reasons"]


def test_auto_recommend_rejects_without_clear_majority():
    helper = EpisodeFormatRuleHelper()
    samples = [
        _make_file("[A] Show [01].mkv"),
        _make_file("[B] Show [02].mkv"),
    ]

    state, errmsg, data = helper.recommend([], samples)

    assert state is False
    assert errmsg == "样本命名差异过大，建议补充集数定位规则"
    assert data is None


def test_validate_auto_template_checks_expected_episode_consistency():
    helper = EpisodeFormatRuleHelper()
    samples = [
        _AutoRecommendSample(
            file_name="Show - 01.mkv",
            ep_span=(7, 9),
            expected_episode="02",
        )
    ]

    assert helper._validate_auto_template("Show - {ep}.mkv", samples) is False


def test_insert_variable_placeholder_preserves_existing_placeholders():
    helper = EpisodeFormatRuleHelper()
    all_file_names = [
        "Show - 01 [x265_flac][1080p].mkv",
        "Show - 02 [x265_flac][720p].mkv",
    ]
    after_ep_list = [
        " [x265_flac][1080p].mkv",
        " [x265_flac][720p].mkv",
    ]
    template = "Show - {ep} [x265{a}][1080p].mkv"

    updated = helper._insert_variable_placeholder(
        template=template,
        failed_files=[all_file_names[1]],
        after_ep_list=after_ep_list,
        all_file_names=all_file_names,
        placeholder="b",
    )

    assert "{a}" in updated
    assert "{b}" in updated


def test_insert_variable_placeholder_does_not_double_escape_braces():
    helper = EpisodeFormatRuleHelper()
    all_file_names = [
        "Show - 01 {v1}.mkv",
        "Show - 02 {v2}.mkv",
    ]
    after_ep_list = [
        " {v1}.mkv",
        " {v2}.mkv",
    ]
    template = "Show - {ep} {{v1}}.mkv"

    updated = helper._insert_variable_placeholder(
        template=template,
        failed_files=[all_file_names[1]],
        after_ep_list=after_ep_list,
        all_file_names=all_file_names,
        placeholder="a",
    )

    assert "{{{{" not in updated
    assert "{a}" in updated


def test_auto_recommend_validates_all_majority_samples():
    helper = EpisodeFormatRuleHelper()
    samples = [
        _make_file(f"Show - {index:02d}.mkv")
        for index in range(1, 11)
    ]
    samples.append(_make_file("Show - 11 [WEB].mkv"))

    state, errmsg, data = helper.recommend([], samples)

    assert state is False
    assert errmsg == "无匹配自定义定位规则，智能生成失败"
    assert data is None


def test_auto_recommend_returns_false_when_parse_raises(monkeypatch):
    helper = EpisodeFormatRuleHelper()
    samples = [
        _make_file("Show - 01 [1080p].mkv"),
        _make_file("Show - 02 [720p].mkv"),
    ]

    def _raise_parse(*args, **kwargs):
        raise ValueError("broken parse")

    monkeypatch.setattr("app.helper.format.parse.parse", _raise_parse)

    state, errmsg, data = helper.recommend([], samples)

    assert state is False
    assert errmsg == "无匹配自定义定位规则，智能生成失败"
    assert data is None


@pytest.mark.parametrize(
    ("file_name", "expected"),
    [
        ("Show 第01話.mkv", "01"),
        ("Show 第01话.mkv", "01"),
        ("Show 第01集.mkv", "01"),
        ("Show。01 1080p.mkv", "01"),
    ],
)
def test_extract_episode_fallback_supports_cjk_patterns(file_name: str, expected: str):
    helper = EpisodeFormatRuleHelper()

    assert helper._extract_episode_fallback(file_name) == expected


def test_auto_recommend_is_stable_when_sample_order_changes():
    helper = EpisodeFormatRuleHelper()
    samples = [
        _make_file("Show - 01 [1080p].mkv"),
        _make_file("Show - 02 [720p].mkv"),
        _make_file("Show - 03 [1080p].mkv"),
    ]

    state1, errmsg1, data1 = helper.recommend([], samples)
    state2, errmsg2, data2 = helper.recommend([], list(reversed(samples)))

    assert state1 is True
    assert errmsg1 == ""
    assert state2 is True
    assert errmsg2 == ""
    assert data1["episode_format"] == data2["episode_format"]
    assert data1["sample_file"] == data2["sample_file"]
    assert data1["sample_count"] == data2["sample_count"]
    assert data1["majority_count"] == data2["majority_count"]


def test_auto_recommend_supports_hash_episode_names():
    helper = EpisodeFormatRuleHelper()
    samples = [
        _make_file("[AI-Raws] 不滅のあなたへ #01 (BD HEVC 1920x1080 yuv444p10le FLAC)[DE0EC3BA].mkv"),
        _make_file("[AI-Raws] 不滅のあなたへ #02 (BD HEVC 1920x1080 yuv444p10le FLAC)[8CE75F1B].mkv"),
        _make_file("[AI-Raws] 不滅のあなたへ #03 (BD HEVC 1920x1080 yuv444p10le FLAC)[986E42F9].mkv"),
    ]

    state, errmsg, data = helper.recommend([], samples)

    assert state is True
    assert errmsg == ""
    assert data["confidence"] == "high"
    assert data["sample_count"] == 3
    assert data["majority_count"] == 3
    assert data["episode_format"] == "[AI-Raws] 不滅のあなたへ #{ep} (BD HEVC 1920x1080 yuv444p10le FLAC)[{a}].mkv"


def test_auto_recommend_ignores_episode_zero_specials():
    helper = EpisodeFormatRuleHelper()
    samples = [
        _make_file("[VCB] Show [00][1080p][x264_2flac].mkv"),
        _make_file("[VCB] Show [01][1080p][x264_2flac].mkv"),
        _make_file("[VCB] Show [02][1080p][x264_2flac].mkv"),
        _make_file("[VCB] Show [12][1080p][x264_3flac].mkv"),
    ]

    state, errmsg, data = helper.recommend([], samples)

    assert state is True
    assert errmsg == ""
    assert data["sample_count"] == 3
    assert data["majority_count"] == 3
    assert data["episode_format"] == "[VCB] Show [{ep}][1080p][x264_{a}flac].mkv"


def test_auto_recommend_validates_media_subtitle_and_audio_together():
    helper = EpisodeFormatRuleHelper()
    sample_names = [
        "Show - 01.mkv",
        "Show - 02.mkv",
        "Show - 01.ass",
        "Show - 02.ass",
        "Show - 01.mka",
        "Show - 02.mka",
    ]
    samples = [_make_file(name) for name in sample_names]

    state, errmsg, data = helper.recommend([], samples)

    assert state is True
    assert errmsg == ""
    assert data["sample_count"] == 6
    parser = FormatParser(eformat=data["episode_format"])
    for sample_name in sample_names:
        assert parser.match(sample_name) is True


def test_auto_recommend_ignores_non_integer_episode_samples():
    helper = EpisodeFormatRuleHelper()
    sample_names = [
        "[T.H.X&VCB-Studio] Hyouka [01][Ma10p_1080p][x265_flac_aac].mkv",
        "[T.H.X&VCB-Studio] Hyouka [02][Ma10p_1080p][x265_flac_aac].mkv",
        "[T.H.X&VCB-Studio] Hyouka [01][Ma10p_1080p][x265_flac_aac].chs.ass",
        "[T.H.X&VCB-Studio] Hyouka [02][Ma10p_1080p][x265_flac_aac].chs.ass",
        "[T.H.X&VCB-Studio] Hyouka [01][Ma10p_1080p][x265_flac_aac].cht.ass",
        "[T.H.X&VCB-Studio] Hyouka [02][Ma10p_1080p][x265_flac_aac].cht.ass",
        "[T.H.X&VCB-Studio] Hyouka [11.5][Ma10p_1080p][x265_flac_aac].mkv",
        "[T.H.X&VCB-Studio] Hyouka [11.5][Ma10p_1080p][x265_flac_aac].chs.ass",
        "[T.H.X&VCB-Studio] Hyouka [11.5][Ma10p_1080p][x265_flac_aac].cht.ass",
    ]
    samples = [_make_file(name) for name in sample_names]

    state, errmsg, data = helper.recommend([], samples)

    assert state is True
    assert errmsg == ""
    assert data["sample_count"] == 6
    assert data["majority_count"] == 6
    assert data["episode_format"] == (
        "[T.H.X&VCB-Studio] Hyouka [{ep}][Ma10p_1080p][x265_flac_aac].{a}"
    )


def test_auto_recommend_ignores_special_sp_samples():
    helper = EpisodeFormatRuleHelper()
    sample_names = [
        "[Tonikaku Kawaii S2][01][BDRIP][1080P][H264_FLAC].mkv",
        "[Tonikaku Kawaii S2][02][BDRIP][1080P][H264_FLAC].mkv",
        "[Tonikaku Kawaii S2][03][BDRIP][1080P][H264_FLAC].mkv",
        "[Tonikaku Kawaii S2][BD-BOX][Disc 02][SP01][NCOP Ver.1][BDRIP][1080P][H264_FLAC].mkv",
        "[Tonikaku Kawaii S2][BD-BOX][Disc 02][SP03][NCED Ver.1][BDRIP][1080P][H264_FLAC].mkv",
        "[Tonikaku Kawaii S2][BD-BOX][Disc 02][SP04][NCED Ver.2][BDRIP][1080P][H264_FLAC].mkv",
    ]
    samples = [_make_file(name) for name in sample_names]

    state, errmsg, data = helper.recommend([], samples)

    assert state is True
    assert errmsg == ""
    assert data["sample_count"] == 3
    assert data["majority_count"] == 3
    assert data["episode_format"] == "[Tonikaku Kawaii S2][{ep}][BDRIP][1080P][H264_FLAC].mkv"


def test_auto_recommend_uses_native_episode_as_fallback(monkeypatch):
    helper = EpisodeFormatRuleHelper()
    samples = [
        _make_file("Show - 01.mkv"),
        _make_file("Show - 02.mkv"),
    ]

    monkeypatch.setattr(
        "app.helper.format.anitopy.parse",
        lambda _: {},
    )
    monkeypatch.setattr(
        helper,
        "_extract_episode_fallback",
        lambda _: None,
    )
    monkeypatch.setattr(
        helper,
        "_extract_native_episode",
        lambda item: "01" if item.name.endswith("01.mkv") else "02",
    )

    state, errmsg, data = helper.recommend([], samples)

    assert state is True
    assert errmsg == ""
    assert data["native_fallback_count"] == 2
    assert data["native_verified_count"] == 0
    assert "native_meta_fallback" in data["reasons"]
    assert data["episode_format"] == "Show - {ep}.mkv"


def test_auto_recommend_rejects_when_native_episode_conflicts(monkeypatch):
    helper = EpisodeFormatRuleHelper()
    samples = [
        _make_file("Show - 01.mkv"),
        _make_file("Show - 02.mkv"),
    ]

    native_values = iter(["02", "02"])
    monkeypatch.setattr(
        helper,
        "_extract_native_episode",
        lambda item: "02",
    )

    state, errmsg, data = helper.recommend([], samples)

    assert state is False
    assert errmsg == "样本命名与原生识别结果冲突，建议补充集数定位规则"
    assert data is None


def test_auto_recommend_marks_native_verified_samples(monkeypatch):
    helper = EpisodeFormatRuleHelper()
    samples = [
        _make_file("Show.S01E01.mkv"),
        _make_file("Show.S01E02.mkv"),
    ]

    monkeypatch.setattr(
        helper,
        "_extract_native_episode",
        lambda item: "01" if item.name.endswith("01.mkv") else "02",
    )

    state, errmsg, data = helper.recommend([], samples)

    assert state is True
    assert errmsg == ""
    assert data["native_verified_count"] == 2
    assert data["native_fallback_count"] == 0
    assert "native_meta_verified" in data["reasons"]


def test_transfer_chain_recommend_episode_format_passes_helper_data(monkeypatch):
    chain = object.__new__(TransferChain)
    file_item = FileItem(
        storage="local",
        path="/downloads/Show/Show - 01.mkv",
        type="file",
        name="Show - 01.mkv",
        extension="mkv",
    )
    directory = FileItem(
        storage="local",
        path="/downloads/Show",
        type="dir",
        name="Show",
    )
    sample = _make_file("Show - 01.mkv")
    helper_data = {
        "rule_name": "智能分析",
        "episode_format": "Show - {ep}.mkv",
        "sample_file": "Show - 01.mkv",
        "pattern": None,
        "sample_count": 1,
        "majority_count": 1,
        "confidence": "low",
        "size_filter_relaxed": False,
        "native_verified_count": 0,
        "native_fallback_count": 0,
        "native_conflict_count": 0,
        "reason": "single_sample_only",
        "reasons": ["single_sample_only"],
        "message": "样本不足，仅基于单文件智能生成（仅供参考）",
    }

    monkeypatch.setattr(
        chain,
        "_TransferChain__resolve_episode_format_directory",
        lambda item: directory,
    )
    monkeypatch.setattr(
        chain,
        "_TransferChain__get_episode_format_rules",
        lambda: [],
    )
    monkeypatch.setattr(
        chain,
        "_TransferChain__get_episode_format_sample_files",
        lambda item: [sample],
    )
    monkeypatch.setattr(
        "app.chain.transfer.EpisodeFormatRuleHelper.recommend",
        lambda self, rules, sample_files: (True, "", helper_data),
    )

    state, errmsg, data = TransferChain.recommend_episode_format(chain, file_item)

    assert state is True
    assert errmsg == ""
    assert data == helper_data


def test_transfer_chain_recommend_episode_format_uses_selected_fileitems(monkeypatch):
    chain = object.__new__(TransferChain)
    monkeypatch.setattr(chain, "_media_exts", [".mkv", ".mp4"], raising=False)
    monkeypatch.setattr(chain, "_subtitle_exts", [".ass", ".ssa"], raising=False)
    monkeypatch.setattr(chain, "_audio_exts", [".mka", ".aac"], raising=False)
    selected_fileitems = [
        FileItem(
            storage="local",
            path="/downloads/Show/Show - 01.mkv",
            type="file",
            name="Show - 01.mkv",
            extension="mkv",
        ),
        FileItem(
            storage="local",
            path="/downloads/Show/Show - 01.ass",
            type="file",
            name="Show - 01.ass",
            extension="ass",
        ),
    ]
    helper_data = {
        "rule_name": "智能分析",
        "episode_format": "Show - {ep}.{a}",
        "sample_file": "Show - 01.mkv",
        "pattern": None,
        "sample_count": 2,
        "majority_count": 2,
        "confidence": "high",
        "size_filter_relaxed": False,
        "native_verified_count": 0,
        "native_fallback_count": 0,
        "native_conflict_count": 0,
        "reason": "selected_samples",
        "reasons": ["selected_samples"],
        "message": "已基于选中文件生成集数定位模板",
    }

    monkeypatch.setattr(
        chain,
        "_TransferChain__get_episode_format_rules",
        lambda: [],
    )
    monkeypatch.setattr(
        "app.chain.transfer.EpisodeFormatRuleHelper.recommend",
        lambda self, rules, sample_files: (True, "", {
            **helper_data,
            "received_samples": [item.name for item in sample_files],
        }),
    )

    state, errmsg, data = TransferChain.recommend_episode_format(
        chain,
        fileitem=None,
        fileitems=selected_fileitems,
    )

    assert state is True
    assert errmsg == ""
    assert data["received_samples"] == [item.name for item in selected_fileitems]


def test_transfer_chain_recommend_episode_format_rejects_invalid_selected_fileitems():
    chain = object.__new__(TransferChain)
    chain._media_exts = [".mkv", ".mp4"]
    chain._subtitle_exts = [".ass", ".ssa"]
    chain._audio_exts = [".mka", ".aac"]
    selected_fileitems = [
        FileItem(
            storage="local",
            path="/downloads/Show/Show - 01.mkv",
            type="file",
            name="Show - 01.mkv",
            extension="mkv",
        ),
        FileItem(
            storage="local",
            path="/downloads/Other/Show - 02.mkv",
            type="file",
            name="Show - 02.mkv",
            extension="mkv",
        ),
    ]

    state, errmsg, data = TransferChain.recommend_episode_format(
        chain,
        fileitem=None,
        fileitems=selected_fileitems,
    )

    assert state is False
    assert errmsg == "当前选择不满足智能识别条件"
    assert data is None


def test_transfer_chain_episode_format_samples_include_extra_files(monkeypatch):
    chain = object.__new__(TransferChain)
    directory = FileItem(
        storage="local",
        path="/downloads/Show",
        type="dir",
        name="Show",
    )
    media_item = _make_file("Show - 01.mkv")
    subtitle_item = _make_file("Show - 01.ass")
    audio_item = _make_file("Show - 01.mka")
    other_item = _make_file("Show - 01.txt")

    monkeypatch.setattr(
        "app.chain.transfer.StorageChain.list_files",
        lambda self, item, recursion=False: [
            media_item,
            subtitle_item,
            audio_item,
            other_item,
        ],
    )
    monkeypatch.setattr(chain, "_media_exts", [".mkv", ".mp4"], raising=False)
    monkeypatch.setattr(chain, "_subtitle_exts", [".ass", ".ssa"], raising=False)
    monkeypatch.setattr(chain, "_audio_exts", [".mka", ".aac"], raising=False)

    sample_files = TransferChain._TransferChain__get_episode_format_sample_files(
        chain,
        directory,
    )

    assert [item.name for item in sample_files] == [
        media_item.name,
        subtitle_item.name,
        audio_item.name,
    ]
