import hashlib
import json
import os
import subprocess
import sys
import tempfile
import unittest.mock

import pytest

from gdown.__main__ import file_size
from gdown.__main__ import main
from gdown.cached_download import _assert_filehash
from gdown.cached_download import _compute_filehash
from gdown.download_folder import _GoogleDriveFile

from .conftest import GITHUB_RELEASE_URL

here = os.path.dirname(os.path.abspath(__file__))


def _test_cli_with_md5(*, url_or_id: str, md5: str, options: list[str] | None) -> None:
    # We can't use NamedTemporaryFile because Windows doesn't allow the subprocess
    # to write the file created by the parent process.
    with tempfile.TemporaryDirectory() as d:
        file_path = os.path.join(d, "file")
        cmd = ["gdown", "--no-cookies", url_or_id, "-O", file_path]
        if options is not None:
            cmd.extend(options)
        subprocess.call(cmd)
        assert os.path.exists(file_path)
        _assert_filehash(path=file_path, hash=f"md5:{md5}")


def _test_cli_with_content(*, url_or_id: str, content: str) -> None:
    # We can't use NamedTemporaryFile because Windows doesn't allow the subprocess
    # to write the file created by the parent process.
    with tempfile.TemporaryDirectory() as d:
        file_path = os.path.join(d, "file")
        cmd = ["gdown", "--no-cookies", url_or_id, "-O", file_path]
        subprocess.call(cmd)
        with open(file_path) as f:
            assert f.read() == content


@pytest.mark.network
def test_download_from_url_other_than_gdrive() -> None:
    url = "https://raw.githubusercontent.com/wkentaro/gdown/3.1.0/gdown/__init__.py"
    md5 = "2a51927dde6b146ce56b4d89ebbb5268"
    _test_cli_with_md5(url_or_id=url, md5=md5, options=None)


@pytest.mark.network
def test_download_small_file_from_gdrive() -> None:
    with open(os.path.join(here, "data/file_ids.csv")) as f:
        file_ids = [file_id.strip() for file_id in f]

    for file_id in file_ids:
        try:
            _test_cli_with_content(url_or_id=file_id, content="spam\n")
            break
        except AssertionError as e:
            print(e, file=sys.stderr)
            continue
    else:
        raise AssertionError(f"Failed to download any of the files: {file_ids}")


@pytest.mark.network
def test_download_large_file_from_gdrive() -> None:
    with open(os.path.join(here, "data/file_ids_large.csv")) as f:
        file_id_and_md5s = [[x.strip() for x in file_id.split(",")] for file_id in f]

    for file_id, md5 in file_id_and_md5s:
        try:
            _test_cli_with_md5(url_or_id=file_id, md5=md5, options=None)
            break
        except AssertionError as e:
            print(e, file=sys.stderr)
            continue
    else:
        file_ids, _ = zip(*file_id_and_md5s)
        raise AssertionError(f"Failed to download any of the files: {file_ids}")


@pytest.mark.network
def test_download_and_extract() -> None:
    cmd = f"gdown --no-cookies {GITHUB_RELEASE_URL} -O - | tar zxvf -"
    with tempfile.TemporaryDirectory() as d:
        subprocess.call(cmd, shell=True, cwd=d)
        assert os.path.exists(os.path.join(d, "gdown-4.0.0/gdown/__init__.py"))


@pytest.mark.network
def test_download_folder_from_gdrive() -> None:
    with open(os.path.join(here, "data/folder_ids.csv")) as f:
        folder_id_and_md5s = [
            [x.strip() for x in folder_id.split(",")] for folder_id in f
        ]

    for folder_id, md5 in folder_id_and_md5s:
        with tempfile.TemporaryDirectory() as d:
            cmd = ["gdown", "--no-cookies", folder_id, "-O", d, "--folder"]
            subprocess.call(cmd)

            md5s_actual = []
            for dirpath, dirnames, filenames in os.walk(d):
                for filename in filenames:
                    md5_actual = _compute_filehash(
                        path=os.path.join(dirpath, filename), algorithm="md5"
                    )[len("md5:") :]
                    md5s_actual.append(md5_actual)

            md5_actual = hashlib.md5(
                ("".join(x + "\n" for x in sorted(md5s_actual))).encode()
            ).hexdigest()
        try:
            assert md5_actual == md5
            break
        except AssertionError as e:
            print(e, file=sys.stderr)
    else:
        file_ids, md5s = zip(*folder_id_and_md5s)
        raise AssertionError(f"Failed to download any of the folders: {file_ids}")


@pytest.mark.network
def test_download_a_folder_with_more_than_50_files() -> None:
    url = "https://drive.google.com/drive/folders/1gd3xLkmjT8IckN6WtMbyFZvLR4exRIkn"

    with tempfile.TemporaryDirectory() as d:
        cmd = ["gdown", "--no-cookies", url, "-O", d, "--folder"]
        subprocess.check_call(cmd)

        filenames = sorted(os.listdir(d))
        assert filenames == [f"file_{i:02d}.txt" for i in range(100)]


# def test_download_docs_from_gdrive():
#     file_id = "1TFYNzuZJTgNGzGmjraZ58ZVOh9_YoKeBnU-opWgXQL4"
#     md5 = "6c17d87d3d01405ac5c9bb65ee2d2fc2"
#     _test_cli_with_md5(url_or_id=file_id, md5=md5, options="--format txt")
#
#
# def test_download_spreadsheets_from_gdrive():
#     file_id = "1h6wQX7ATSJDOSWFEjHPmv_nukJzZD_zZ30Jvy6XNiTE"
#     md5 = "5be20dd8a23afa06365714edc24856f3"
#     _test_cli_with_md5(url_or_id=file_id, md5=md5, options="--format pdf")


@pytest.mark.network
def test_download_slides_from_gdrive() -> None:
    file_id = "13AhW1Z1GYGaiTpJ0Pr2TTXoQivb6jx-a"
    md5 = "96704c6c40e308a68d3842e83a0136b9"
    _test_cli_with_md5(url_or_id=file_id, md5=md5, options=["--format", "pdf"])


def test_json_flag_outputs_json_array(
    *, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
    root = _GoogleDriveFile(
        id="root_id",
        name="myfolder",
        type=_GoogleDriveFile.TYPE_FOLDER,
        children=[
            _GoogleDriveFile(
                id="child_id",
                name="track.mp3",
                type="application/octet-stream",
            ),
        ],
    )
    monkeypatch.setattr(
        sys,
        "argv",
        [
            "gdown",
            "https://drive.google.com/drive/folders/dummy",
            "--folder",
            "--json",
        ],
    )
    with unittest.mock.patch.object(
        sys.modules["gdown.download_folder"],
        "_download_and_parse_google_drive_link",
        return_value=root,
    ):
        main()

    captured = capsys.readouterr()
    entries = json.loads(captured.out)
    assert entries == [
        {
            "url": "https://drive.google.com/uc?id=child_id",
            "path": "track.mp3",
        }
    ]


def test_json_flag_preserves_subfolder_path(
    *, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
    root = _GoogleDriveFile(
        id="root_id",
        name="myfolder",
        type=_GoogleDriveFile.TYPE_FOLDER,
        children=[
            _GoogleDriveFile(
                id="sub_id",
                name="album",
                type=_GoogleDriveFile.TYPE_FOLDER,
                children=[
                    _GoogleDriveFile(
                        id="nested_id",
                        name="track.mp3",
                        type="application/octet-stream",
                    ),
                ],
            ),
        ],
    )
    monkeypatch.setattr(
        sys,
        "argv",
        [
            "gdown",
            "https://drive.google.com/drive/folders/dummy",
            "--folder",
            "--json",
        ],
    )
    with unittest.mock.patch.object(
        sys.modules["gdown.download_folder"],
        "_download_and_parse_google_drive_link",
        return_value=root,
    ):
        main()

    captured = capsys.readouterr()
    entries = json.loads(captured.out)
    assert entries == [
        {
            "url": "https://drive.google.com/uc?id=nested_id",
            "path": "album/track.mp3",
        }
    ]


def test_json_flag_does_not_download(
    *,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    root = _GoogleDriveFile(
        id="root_id",
        name="myfolder",
        type=_GoogleDriveFile.TYPE_FOLDER,
        children=[
            _GoogleDriveFile(
                id="child_id",
                name="track.mp3",
                type="application/octet-stream",
            ),
        ],
    )
    monkeypatch.setattr(
        sys,
        "argv",
        [
            "gdown",
            "https://drive.google.com/drive/folders/dummy",
            "--folder",
            "--json",
        ],
    )
    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"
        ) as mock_download,
    ):
        main()

    mock_download.assert_not_called()


@pytest.mark.parametrize("output", ["out.bin", "-"])
def test_json_flag_rejects_output(*, output: str) -> None:
    cmd = [
        sys.executable,
        "-m",
        "gdown",
        "https://drive.google.com/file/d/dummy/view",
        "--json",
        "-O",
        output,
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, check=False)
    assert result.returncode != 0
    assert "--json cannot be combined with -O/--output" in result.stderr


def _fake_session_returning(*, headers: dict[str, str]) -> unittest.mock.Mock:
    response = unittest.mock.Mock()
    response.status_code = 200
    response.url = "https://drive.google.com/uc?id=child_id"
    response.headers = headers

    sess = unittest.mock.Mock()
    sess.get.return_value = response
    return sess


def test_json_flag_single_file_outputs_array(
    *, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
    monkeypatch.setattr(
        sys,
        "argv",
        [
            "gdown",
            "--no-cookies",
            "https://drive.google.com/file/d/child_id/view",
            "--json",
        ],
    )
    sess = _fake_session_returning(
        headers={
            "Content-Type": "application/octet-stream",
            "Content-Disposition": 'attachment; filename="video.webm"',
        }
    )
    with unittest.mock.patch.object(
        sys.modules["gdown.download"], "_get_session", return_value=(sess, "")
    ):
        main()

    captured = capsys.readouterr()
    entries = json.loads(captured.out)
    assert entries == [
        {
            "url": "https://drive.google.com/uc?id=child_id",
            "path": "video.webm",
        }
    ]


def test_json_flag_single_file_without_drive_filename_raises(
    *,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setattr(
        sys,
        "argv",
        [
            "gdown",
            "--no-cookies",
            "https://example.com/file",
            "--json",
        ],
    )
    sess = _fake_session_returning(headers={"Content-Type": "application/octet-stream"})
    with (
        unittest.mock.patch.object(
            sys.modules["gdown.download"], "_get_session", return_value=(sess, "")
        ),
        pytest.raises(SystemExit),
    ):
        main()


@pytest.mark.parametrize(
    "argv, expected",
    [
        ("100B", 100.0),
        ("2KB", 2.0 * 1024),
        ("3MB", 3.0 * 1024**2),
        ("4GB", 4.0 * 1024**3),
    ],
)
def test_file_size_parses_units(*, argv: str, expected: float) -> None:
    assert file_size(argv) == expected


def test_file_size_none_returns_none() -> None:
    assert file_size(None) is None


def test_file_size_without_unit_raises_type_error() -> None:
    with pytest.raises(TypeError):
        file_size("100")
