import os.path as osp
import sys
import tempfile
import unittest.mock
from pathlib import Path
from typing import Final

import pytest

from gdown.download_folder import _GoogleDriveFile
from gdown.download_folder import _parse_embedded_folder_view
from gdown.download_folder import download_folder
from gdown.exceptions import DownloadError

from .conftest import build_response

here = osp.dirname(osp.abspath(__file__))


@pytest.mark.network
def test_download_folder_google_slides_without_extension(*, tmp_path: Path) -> None:
    # The folder contains a Google Slides file named "gdown" with no extension in
    # Google Drive. Previously, download_folder() passed this extensionless name as
    # the output path to download(), which saved the file without .pptx extension.
    # The fix passes the directory instead, letting download() resolve the filename
    # (including extension) from the Content-Disposition header.
    url = "https://drive.google.com/drive/folders/12zxlvJtuHFV6awc3AINaNHnfvRttPv0i"
    files = download_folder(url=url, output=str(tmp_path), quiet=True)
    assert len(files) == 1
    assert isinstance(files[0], str)
    assert files[0].endswith(".pptx")


def _make_folder_root(*, name: str, child_name: str) -> _GoogleDriveFile:
    return _GoogleDriveFile(
        id="root_id",
        name=name,
        type=_GoogleDriveFile.TYPE_FOLDER,
        children=[
            _GoogleDriveFile(
                id="child_id",
                name=child_name,
                type="text/plain",
            ),
        ],
    )


def test_root_folder_name_path_traversal_is_sanitized(*, tmp_path: Path) -> None:
    root = _make_folder_root(name="../../evil", child_name="safe_file.txt")
    output_dir = str(tmp_path) + osp.sep

    with unittest.mock.patch.object(
        sys.modules["gdown.download_folder"],
        "_download_and_parse_google_drive_link",
        return_value=root,
    ):
        files = download_folder(
            url="https://drive.google.com/drive/folders/dummy",
            output=output_dir,
            skip_download=True,
            quiet=True,
        )

    for file in files:
        assert not isinstance(file, str)
        resolved = osp.realpath(file.local_path)
        assert resolved.startswith(osp.realpath(output_dir))


def test_download_folder_propagates_download_error(*, tmp_path: Path) -> None:
    root = _make_folder_root(name="folder", child_name="file.txt")

    with (
        unittest.mock.patch.object(
            sys.modules["gdown.download_folder"],
            "_download_and_parse_google_drive_link",
            return_value=root,
        ),
        unittest.mock.patch.object(
            sys.modules["gdown.download_folder"],
            "download",
            side_effect=DownloadError("access denied"),
        ),
        pytest.raises(DownloadError, match="access denied"),
    ):
        download_folder(
            url="https://drive.google.com/drive/folders/dummy",
            output=str(tmp_path) + osp.sep,
            quiet=True,
        )


def test_download_folder_closes_session_when_parsing_raises() -> None:
    session = unittest.mock.Mock()

    with (
        unittest.mock.patch.object(
            sys.modules["gdown.download_folder"],
            "_get_session",
            return_value=(session, "cookies.txt"),
        ),
        unittest.mock.patch.object(
            sys.modules["gdown.download_folder"],
            "_download_and_parse_google_drive_link",
            side_effect=DownloadError("parse failed"),
        ),
        pytest.raises(DownloadError, match="parse failed"),
    ):
        download_folder(id="folder_id", quiet=True)

    session.close.assert_called_once_with()


def test_parse_embedded_folder_view() -> None:
    html_file = osp.join(here, "data/embedded-folder-view-sample.html")
    with open(html_file) as f:
        content = f.read()

    mock_response = unittest.mock.Mock()
    mock_response.status_code = 200
    mock_response.text = content

    mock_sess = unittest.mock.Mock()
    mock_sess.get.return_value = mock_response

    result = _parse_embedded_folder_view(
        sess=mock_sess, folder_id="test_folder_id", verify=True
    )

    assert result is not None
    folder_name, children = result
    assert folder_name == "files_100"
    ids = [r[0] for r in children]
    names = [r[1] for r in children]
    types = [r[2] for r in children]

    assert ids == [
        "108RHF3bQb6dgOByv_KMGzHuktJOwU_jL",
        "1Sul7bhaimPjncS2GE73nVloSPQbtyzu-",
        "1xYz2AbCdEfGhIjKlMnOpQrStUvWxYz3A",
        "1aMZqPaU03E7XOQNXtjSCdguRHBaIQ82m",
    ]
    assert names == ["file_00.txt", "file_01.txt", "photo.jpg", "subfolder"]
    assert types == [
        "application/octet-stream",
        "application/octet-stream",
        "application/octet-stream",
        _GoogleDriveFile.TYPE_FOLDER,
    ]


def test_parse_embedded_folder_view_http_error() -> None:
    mock_response = unittest.mock.Mock()
    mock_response.status_code = 404

    mock_sess = unittest.mock.Mock()
    mock_sess.get.return_value = mock_response

    with pytest.raises(DownloadError, match="status code 404"):
        _parse_embedded_folder_view(
            sess=mock_sess, folder_id="nonexistent", verify=True
        )


def test_parse_embedded_folder_view_malformed_html() -> None:
    mock_response = unittest.mock.Mock()
    mock_response.status_code = 200
    mock_response.text = "<html><body>no title</body></html>"

    mock_sess = unittest.mock.Mock()
    mock_sess.get.return_value = mock_response

    with pytest.raises(DownloadError, match="page structure may have changed"):
        _parse_embedded_folder_view(sess=mock_sess, folder_id="test", verify=True)


@pytest.mark.network
def test_download_folder_dry_run() -> None:
    EXPECTED_FILE_COUNT: Final = 6
    url = "https://drive.google.com/drive/folders/1KpLl_1tcK0eeehzN980zbG-3M2nhbVks"
    tmp_dir = tempfile.mkdtemp()
    files = download_folder(url=url, output=tmp_dir, skip_download=True)
    assert len(files) == EXPECTED_FILE_COUNT
    for file in files:
        assert hasattr(file, "id")
        assert hasattr(file, "path")
        assert hasattr(file, "local_path")


def test_download_folder_fails_when_a_file_ends_before_announced_size(
    *, tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
    root = _make_folder_root(name="folder", child_name="file.txt")
    response = build_response(
        headers={
            "Content-Disposition": 'attachment; filename="file.txt"',
            "Content-Length": "10",
        },
        chunks=[b"data"],
    )
    session = unittest.mock.Mock()
    session.get.return_value = response
    session.cookies = []

    with (
        unittest.mock.patch.object(
            sys.modules["gdown.download_folder"],
            "_download_and_parse_google_drive_link",
            return_value=root,
        ),
        unittest.mock.patch.object(
            sys.modules["gdown.download"],
            "_get_session",
            return_value=(session, str(tmp_path / "cookies.txt")),
        ),
        pytest.raises(DownloadError, match="received 4 bytes"),
    ):
        download_folder(
            url="https://drive.google.com/drive/folders/dummy",
            output=str(tmp_path) + osp.sep,
            quiet=False,
        )

    assert "Download completed" not in capsys.readouterr().err
    assert not (tmp_path / "folder" / "file.txt").exists()
    part_files = list((tmp_path / "folder").glob("file.txt*.part"))
    assert len(part_files) == 1
    assert part_files[0].read_bytes() == b"data"
