"""
整理历史的写入路径：add_transfer_success / add_transfer_fail。

这两个函数是整理历史表的**唯一**写入口，整理链的每一次成败都经由它们落库。
它们干的不是数据访问，而是把 FileItem / MetaBase / MediaInfo / TransferInfo
四个领域对象翻译成一行历史——字段映射错了不会报错，只会让历史里的季集、标题、
来源静静地记错，而整理查重、媒体库溯源、失败重试全都读这张表。

因此这里断言的是「落库后每个字段的实际值」，不是「调用了什么」。

它们与同一张表的读侧规则（查重闸，见 test_transfer_history_gate.py）同住
app/application/history.py；此前长在 TransferHistoryOper 上，故本文件旧名为
test_db_transferhistory_write_path.py。
"""
import pytest

from app import schemas
from app.application.history import add_transfer_fail, add_transfer_success
from app.domain.context import MediaInfo
from app.domain.metainfo import MetaInfo
from app.db.models.transferhistory import TransferHistory
from app.db.oper.transferhistory import TransferHistoryOper
from app.schemas.types import MediaSource, MediaType


@pytest.fixture(autouse=True)
def _track(db):
    """把整理历史表纳入用例级回收。"""
    db.watermark(TransferHistory)


def _fileitem(path: str, storage: str = "local") -> schemas.FileItem:
    """构造源文件项。"""
    return schemas.FileItem(storage=storage, path=path,
                            name=path.rsplit("/", 1)[-1], type="file")


def _transferinfo(dest: str = "/media/片名/Season 01/片名 - S01E02.mkv",
                  message: str = None, with_target: bool = True) -> schemas.TransferInfo:
    """构造整理结果。"""
    target = _fileitem(dest) if with_target else None
    return schemas.TransferInfo(success=not message,
                                fileitem=_fileitem("/downloads/片名.S01E02.2026.mkv"),
                                target_item=target,
                                file_list=["/downloads/片名.S01E02.2026.mkv"],
                                message=message)


def _mediainfo(title: str = "识别标题", mtype: MediaType = MediaType.TV,
               year: str = "2026", category: str = "国产剧",
               media_id: str = "5566", episode_group: str = None) -> MediaInfo:
    """构造识别结果。"""
    media = MediaInfo()
    media.type = mtype
    media.title = title
    media.year = year
    media.category = category
    media.media_source = MediaSource.TMDB
    media.media_id = media_id
    media.episode_group = episode_group
    return media


# --------------------------------------------------------------------------- #
# add_transfer_success
# --------------------------------------------------------------------------- #

def test_add_success_maps_every_field_onto_the_row(db):
    """
    成功整理的字段映射必须完整落库。

    源/目标各自带存储、季集来自识别结果、状态为成功——这些是整理查重与媒体库
    溯源的全部依据，错一项就查不回来。
    """
    oper = TransferHistoryOper(db=db.session)
    meta = MetaInfo("片名.S01E02.2026.mkv")

    add_transfer_success(transfer_history_oper=oper,
                         fileitem=_fileitem("/downloads/片名.S01E02.2026.mkv"),
                         mode="move", meta=meta, mediainfo=_mediainfo(),
                         transferinfo=_transferinfo(),
                         downloader="qbittorrent", download_hash="hash-ok")

    row = oper.get_by_src("/downloads/片名.S01E02.2026.mkv")
    assert row.status is True
    assert (row.src_storage, row.dest_storage) == ("local", "local")
    assert row.dest == "/media/片名/Season 01/片名 - S01E02.mkv"
    assert (row.mode, row.type, row.category) == ("move", MediaType.TV.value, "国产剧")
    assert (row.title, row.year) == ("识别标题", "2026")
    assert (row.seasons, row.episodes) == ("S01", "E02")
    assert (row.downloader, row.download_hash) == ("qbittorrent", "hash-ok")
    assert row.files == ["/downloads/片名.S01E02.2026.mkv"]
    assert row.errmsg is None


def test_add_success_persists_both_file_items(db):
    """
    源与目标的完整文件项都要落库。

    整理链回滚、重新整理都要拿原始 FileItem 复原，只存路径字符串不够。
    """
    oper = TransferHistoryOper(db=db.session)
    add_transfer_success(transfer_history_oper=oper,
                         fileitem=_fileitem("/downloads/item.mkv"), mode="link",
                         meta=MetaInfo("item.mkv"), mediainfo=_mediainfo(),
                         transferinfo=_transferinfo(dest="/media/item.mkv"))

    row = oper.get_by_src("/downloads/item.mkv")
    assert row.src_fileitem["path"] == "/downloads/item.mkv"
    assert row.dest_fileitem["path"] == "/media/item.mkv"


def test_add_success_tolerates_missing_target_item(db):
    """
    没有目标文件项时，目标三项一并留空而不是崩。

    某些存储的整理只回传结果不回传条目，硬取 target_item.path 会直接抛。
    """
    oper = TransferHistoryOper(db=db.session)
    add_transfer_success(transfer_history_oper=oper,
                         fileitem=_fileitem("/downloads/no-target.mkv"), mode="copy",
                         meta=MetaInfo("no-target.mkv"), mediainfo=_mediainfo(),
                         transferinfo=_transferinfo(with_target=False))

    row = oper.get_by_src("/downloads/no-target.mkv")
    assert (row.dest, row.dest_storage, row.dest_fileitem) == (None, None, None)
    assert row.status is True


def test_add_success_replaces_the_previous_record_of_the_same_source(db):
    """
    同一源路径重复整理只保留最新一条。

    经 add_force 的先删后插实现；留下旧行会让查重命中过期的目标路径。
    """
    oper = TransferHistoryOper(db=db.session)
    for title in ("第一次", "第二次"):
        add_transfer_success(transfer_history_oper=oper,
                             fileitem=_fileitem("/downloads/dup.mkv"), mode="move",
                             meta=MetaInfo("dup.mkv"),
                             mediainfo=_mediainfo(title=title),
                             transferinfo=_transferinfo())

    rows = oper.list_success_by_src("/downloads/dup.mkv")
    assert [r.title for r in rows] == ["第二次"]


def test_add_success_prefers_track_title_for_music(db):
    """
    音乐文件记录曲目标题，而不是识别出的专辑/艺人名。

    音乐库按曲目组织，记成专辑名会让单曲在历史里全部重名、无法区分。
    """
    oper = TransferHistoryOper(db=db.session)
    music_meta = MetaInfo("周杰伦 - 晴天.flac")

    add_transfer_success(transfer_history_oper=oper,
                         fileitem=_fileitem("/downloads/晴天.flac"), mode="move",
                         meta=music_meta,
                         mediainfo=_mediainfo(title="叶惠美", mtype=MediaType.MUSIC),
                         transferinfo=_transferinfo(dest="/media/晴天.flac"))

    assert oper.get_by_src("/downloads/晴天.flac").title == "晴天"


def test_add_success_falls_back_to_recognized_title(db):
    """
    非音乐文件用识别标题，识别标题为空时退回文件名解析出的名字。
    """
    oper = TransferHistoryOper(db=db.session)
    meta = MetaInfo("片名.S01E02.2026.mkv")

    add_transfer_success(transfer_history_oper=oper,
                         fileitem=_fileitem("/downloads/fallback.mkv"), mode="move",
                         meta=meta, mediainfo=_mediainfo(title=None),
                         transferinfo=_transferinfo())

    assert oper.get_by_src("/downloads/fallback.mkv").title == meta.name


# --------------------------------------------------------------------------- #
# add_transfer_fail
# --------------------------------------------------------------------------- #

def test_add_fail_records_the_transfer_error_message(db):
    """
    整理失败时状态为失败，并保留具体错误信息。

    失败重试与人工排障都只能看这条 errmsg。
    """
    oper = TransferHistoryOper(db=db.session)

    add_transfer_fail(transfer_history_oper=oper,
                      fileitem=_fileitem("/downloads/fail.mkv"), mode="move",
                      meta=MetaInfo("fail.mkv"), mediainfo=_mediainfo(),
                      transferinfo=_transferinfo(message="目标路径不可写"))

    row = oper.get_by_src("/downloads/fail.mkv")
    assert row.status is False
    assert row.errmsg == "目标路径不可写"
    assert row.title == "识别标题"


def test_add_fail_uses_a_default_message_when_none_given(db):
    """
    整理结果没带错误信息时落一个兜底文案，不能留空。

    留空会让失败记录在界面上显示成「无错误」，与成功记录无从区分。
    """
    oper = TransferHistoryOper(db=db.session)

    add_transfer_fail(transfer_history_oper=oper,
                      fileitem=_fileitem("/downloads/nomsg.mkv"), mode="move",
                      meta=MetaInfo("nomsg.mkv"), mediainfo=_mediainfo(),
                      transferinfo=_transferinfo(message=""))

    assert oper.get_by_src("/downloads/nomsg.mkv").errmsg == "未知错误"


def test_add_fail_persists_episode_group(db):
    """
    失败记录要带上剧集组——重试时靠它还原到同一个剧集组，否则会整理错季。
    """
    oper = TransferHistoryOper(db=db.session)

    add_transfer_fail(transfer_history_oper=oper,
                      fileitem=_fileitem("/downloads/eg.mkv"), mode="move",
                      meta=MetaInfo("eg.mkv"),
                      mediainfo=_mediainfo(episode_group="eg-1"),
                      transferinfo=_transferinfo(message="出错"))

    assert oper.get_by_src("/downloads/eg.mkv").episode_group == "eg-1"


def test_add_fail_without_recognition_takes_the_unidentified_branch(db):
    """
    未识别到媒体信息时走另一条分支：错误文案固定，且不写目标路径。

    这条分支是「文件识别不出来」的唯一记录方式，丢了这些文件就彻底无迹可寻。
    """
    oper = TransferHistoryOper(db=db.session)
    meta = MetaInfo("无法识别的文件.S02E05.2020.mkv")

    add_transfer_fail(transfer_history_oper=oper,
                      fileitem=_fileitem("/downloads/unknown.mkv"), mode="move",
                      meta=meta)

    row = oper.get_by_src("/downloads/unknown.mkv")
    assert row.status is False
    assert row.errmsg == "未识别到媒体信息"
    assert row.dest is None
    assert (row.seasons, row.episodes) == ("S02", "E05")
    assert row.year == meta.year
    assert row.title == meta.name


def test_add_fail_unidentified_music_is_marked_as_a_recording(db):
    """
    未识别的音乐文件按单曲登记实体类型。

    音乐订阅按单曲/专辑分别查重，实体类型为空会让这条历史两边都匹配不上。
    """
    oper = TransferHistoryOper(db=db.session)

    add_transfer_fail(transfer_history_oper=oper,
                      fileitem=_fileitem("/downloads/unknown.flac"), mode="move",
                      meta=MetaInfo("周杰伦 - 晴天.flac"))

    row = oper.get_by_src("/downloads/unknown.flac")
    assert row.music_type == "recording"
    assert row.type == MediaType.MUSIC.value
    assert row.title == "晴天"


def test_add_fail_returns_the_persisted_row(db):
    """
    两条分支都要把落库后的记录返回，调用方据此拿主键做后续关联。
    """
    oper = TransferHistoryOper(db=db.session)

    identified = add_transfer_fail(transfer_history_oper=oper,
                                   fileitem=_fileitem("/downloads/r1.mkv"), mode="move",
                                   meta=MetaInfo("r1.mkv"), mediainfo=_mediainfo(),
                                   transferinfo=_transferinfo(message="错误"))
    unidentified = add_transfer_fail(transfer_history_oper=oper,
                                     fileitem=_fileitem("/downloads/r2.mkv"), mode="move",
                                     meta=MetaInfo("r2.mkv"))

    assert identified.id is not None
    assert unidentified.id is not None
    assert identified.id != unidentified.id
