import logging
from pathlib import Path
from typing import Any

import pytest

from app.library.Download import Download, NestedLogger, Terminator
from app.library.ItemDTO import ItemDTO


class CaptureHandler(logging.Handler):
    def __init__(self) -> None:
        super().__init__()
        self.records: list[logging.LogRecord] = []

    def emit(self, record: logging.LogRecord) -> None:
        self.records.append(record)


def make_item(id: str = "id1", title: str = "T", url: str = "http://u", folder: str = "f") -> ItemDTO:
    return ItemDTO(id=id, title=title, url=url, folder=folder)


class DummyQueue:
    def __init__(self) -> None:
        self.items: list[Any] = []

    def put(self, obj: Any) -> None:
        self.items.append(obj)

    def get(self) -> Any:
        if not self.items:
            return None
        return self.items.pop(0)


class TestNestedLogger:
    def test_debug_maps_levels_and_strips_prefix(self) -> None:
        logger = logging.getLogger("nl_test")
        logger.setLevel(logging.DEBUG)
        cap = CaptureHandler()
        # Remove existing handlers to avoid duplicates
        for h in list(logger.handlers):
            logger.removeHandler(h)
        logger.addHandler(cap)

        nl = NestedLogger(logger)
        nl.debug("[debug] detail")
        nl.debug("[download] progress")
        nl.debug("[info] info message")

        # Two DEBUG, one INFO
        levels = [r.levelno for r in cap.records]
        assert levels.count(logging.DEBUG) == 2
        assert levels.count(logging.INFO) == 1
        msgs = [r.getMessage() for r in cap.records]
        assert "[debug]" not in msgs[0]
        # [download] prefix is not stripped by NestedLogger
        assert msgs[1] == "[download] progress"
        assert msgs[2] == "info message"


class TestDownloadHooks:
    @pytest.fixture(autouse=True)
    def cfg_and_bus(self, monkeypatch: pytest.MonkeyPatch):
        # Minimal Config stub
        class Cfg:
            debug = False
            ytdlp_debug = False
            max_workers = 1
            temp_keep = False
            temp_disabled = True
            download_info_expires = 3600

            @staticmethod
            def get_instance():
                return Cfg

        monkeypatch.setattr("app.library.Download.Config", Cfg)
        # EventBus.get_instance is used during __init__ and start, we don't hit start here
        class EB:
            @staticmethod
            def get_instance():
                return EB

            @staticmethod
            def emit(*_args, **_kwargs):
                return None

        monkeypatch.setattr("app.library.Download.EventBus", EB)

    def test_progress_hook_filters_fields(self) -> None:
        d = Download(make_item())
        q = DummyQueue()
        d.status_queue = q

        payload = {
            "tmpfilename": "t",
            "filename": "f",
            "status": "downloading",
            "msg": "m",
            "total_bytes": 10,
            "total_bytes_estimate": 12,
            "downloaded_bytes": 5,
            "speed": 1,
            "eta": 2,
            "other": "x",
        }
        d._progress_hook(payload)
        assert len(q.items) == 1
        ev = q.items[0]
        assert ev["id"] == d.id
        assert ev["action"] == "progress"
        # ensure only whitelisted keys included
        assert "other" not in ev
        for k in ("tmpfilename", "filename", "status", "msg", "total_bytes", "total_bytes_estimate", "downloaded_bytes", "speed", "eta"):
            assert k in ev

    def test_postprocessor_hook_movefiles_sets_final_name(self, tmp_path: Path) -> None:
        d = Download(make_item())
        q = DummyQueue()
        d.status_queue = q

        finaldir = tmp_path / "out"
        finaldir.mkdir()
        path = finaldir / "file.mp4"
        # we don't need it to exist for this branch; hook doesn't check file existence
        payload = {
            "postprocessor": "MoveFiles",
            "status": "finished",
            "info_dict": {"__finaldir": str(finaldir), "filepath": str(path)},
        }
        d._postprocessor_hook(payload)
        assert len(q.items) == 1
        ev = q.items[0]
        assert ev["action"] == "moved"
        assert ev["status"] == "finished"
        assert ev["final_name"].endswith("file.mp4")

    def test_post_hooks_pushes_filename(self) -> None:
        d = Download(make_item())
        q = DummyQueue()
        d.status_queue = q
        d.post_hooks(None)
        assert len(q.items) == 0
        d.post_hooks("name.ext")
        assert len(q.items) == 1
        assert q.items[0]["filename"] == "name.ext"


class TestDownloadStale:
    @pytest.fixture(autouse=True)
    def cfg_and_bus(self, monkeypatch: pytest.MonkeyPatch):
        class Cfg:
            debug = False
            ytdlp_debug = False
            max_workers = 1
            temp_keep = False
            temp_disabled = True
            download_info_expires = 3600

            @staticmethod
            def get_instance():
                return Cfg

        monkeypatch.setattr("app.library.Download.Config", Cfg)

        class EB:
            @staticmethod
            def get_instance():
                return EB

            @staticmethod
            def emit(*_args, **_kwargs):
                return None

        monkeypatch.setattr("app.library.Download.EventBus", EB)

    def test_is_stale_conditions(self, monkeypatch: pytest.MonkeyPatch) -> None:
        d = Download(make_item())
        # Auto-start disabled is never stale
        d.info.auto_start = False
        assert d.is_stale() is False

        d.info.auto_start = True
        # Not started yet -> not stale
        d.started_time = 0
        assert d.is_stale() is False

        # Less than 300 seconds elapsed -> not stale
        d.started_time = 1000
        monkeypatch.setattr("time.time", lambda: 1200)
        assert d.is_stale() is False

        # Not running after 300s -> stale
        d.started_time = 0
        d.started_time = 1000
        monkeypatch.setattr("time.time", lambda: 1401)
        monkeypatch.setattr("app.library.Download.Download.running", lambda _self: False)
        d.info.status = "preparing"
        assert d.is_stale() is True

        # Running but status stuck -> stale
        monkeypatch.setattr("app.library.Download.Download.running", lambda _self: True)
        d.info.status = "queued"
        assert d.is_stale() is True

        # Running and in allowed statuses -> not stale
        for s in ("finished", "error", "cancelled", "downloading", "postprocessing"):
            d.info.status = s
            assert d.is_stale() is False


@pytest.mark.asyncio
async def test_start_initializes_and_emits(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    class Cfg:
        debug = False
        ytdlp_debug = False
        max_workers = 1
        temp_keep = False
        temp_disabled = False
        download_info_expires = 3600

        @staticmethod
        def get_instance():
            return Cfg

        class _Mgr:
            @staticmethod
            def Queue():  # noqa: N802
                return DummyQueue()

        @staticmethod
        def get_manager():
            return Cfg._Mgr

    monkeypatch.setattr("app.library.Download.Config", Cfg)

    events = []

    class EB:
        @staticmethod
        def get_instance():
            return EB

        @staticmethod
        def emit(event, data=None, **_kwargs):
            events.append((event, data))

    monkeypatch.setattr("app.library.Download.EventBus", EB)

    class FakeProc:
        def __init__(self, name: str, target, *_args, **_kwargs):
            self.name = name
            self.target = target
            self.started = False

        def start(self) -> None:
            self.started = True

        def join(self) -> int:
            return 0

    monkeypatch.setattr("app.library.Download.multiprocessing.Process", FakeProc)

    item = make_item(id="start1")
    d = Download(item)
    d.temp_dir = str(tmp_path)
    d.download_dir = str(tmp_path)

    await d.start()

    assert d.info.status == "preparing"
    assert isinstance(d.status_queue, DummyQueue)
    assert d.temp_path is not None
    assert Path(d.temp_path).exists()
    assert len(events) >= 1


@pytest.mark.asyncio
async def test_progress_update_processes_statuses(monkeypatch: pytest.MonkeyPatch) -> None:
    class Cfg:
        debug = False
        ytdlp_debug = False
        max_workers = 1
        temp_keep = False
        temp_disabled = True
        download_info_expires = 3600

        @staticmethod
        def get_instance():
            return Cfg

    monkeypatch.setattr("app.library.Download.Config", Cfg)

    class EB:
        @staticmethod
        def get_instance():
            return EB

        @staticmethod
        def emit(*_a, **_kw):
            return None

    monkeypatch.setattr("app.library.Download.EventBus", EB)

    seen = []

    async def spy(_self, status):
        seen.append(status)

    d = Download(make_item(id="p1"))
    d.status_queue = DummyQueue()
    monkeypatch.setattr(Download, "_process_status_update", spy, raising=False)
    d.status_queue.put({"id": d.id, "status": "downloading"})
    d.status_queue.put({"id": d.id, "status": "postprocessing"})
    d.status_queue.put(Terminator())

    await d.progress_update()

    assert [s["status"] for s in seen] == ["downloading", "postprocessing"]


@pytest.mark.asyncio
async def test__process_status_update_finish_sets_fields(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    class Cfg:
        debug = False
        ytdlp_debug = False
        max_workers = 1
        temp_keep = False
        temp_disabled = True
        download_info_expires = 3600

        @staticmethod
        def get_instance():
            return Cfg

    monkeypatch.setattr("app.library.Download.Config", Cfg)

    class EB:
        @staticmethod
        def get_instance():
            return EB

        @staticmethod
        def emit(*_a, **_kw):
            return None

    monkeypatch.setattr("app.library.Download.EventBus", EB)

    class FF:
        def has_video(self):
            return True

        def has_audio(self):
            return True

        metadata = {"duration": "7.9"}

    async def fake_ffprobe(_p):
        return FF()

    monkeypatch.setattr("app.library.Download.ffprobe", fake_ffprobe)

    d = Download(make_item(id="f1"))
    d.download_dir = str(tmp_path)
    path = Path(tmp_path) / "file.bin"
    path.write_bytes(b"x" * 10)

    await d._process_status_update({"id": d.id, "status": "finished", "final_name": str(path)})

    assert d.info.file_size == 10
    assert d.info.extras["is_video"] is True
    assert d.info.extras["is_audio"] is True
    assert d.info.extras["duration"] == int(7.9)
    assert isinstance(d.info.datetime, str)
