import json
import os
import re
import shutil
import sys
from pathlib import Path
from typing import Dict, List, Set, Union
from urllib.parse import urlparse

import pytest

from aqt.exceptions import (
    AqtException,
    ArchiveConnectionError,
    ArchiveDownloadError,
    ArchiveListError,
    CliInputError,
    EmptyMetadata,
)
from aqt.helper import Settings
from aqt.installer import Cli
from aqt.metadata import (
    ArchiveId,
    MetadataFactory,
    QtRepoProperty,
    SimpleSpec,
    ToolData,
    Version,
    Versions,
    show_list,
    suggested_follow_up,
)

Settings.load_settings()


@pytest.mark.parametrize(
    "arch, version, expect",
    (
        ("wasm_32", Version("5.13.1"), "wasm"),
        ("mingw", Version("5.13.1"), ""),
        ("android_fake", Version("5.13.1"), ""),
        ("android_x86", Version("5.13.1"), ""),
        ("android_x86", Version("6.13.1"), "x86"),
        ("android_x86", Version("6.0.0"), "x86"),
    ),
)
def test_list_extension_for_arch(arch: str, version: Version, expect: str):
    ext = QtRepoProperty.extension_for_arch(arch, version >= Version("6.0.0"))
    assert ext == expect


@pytest.mark.parametrize(
    "arch, expect",
    (
        ("wasm_32", ["wasm"]),
        ("mingw", [""]),
        ("android_fake", [""]),
        ("android", [""]),
        ("android_x86", ["", "x86"]),
    ),
)
def test_list_possible_extension_for_arch(arch: str, expect: List[str]):
    exts = QtRepoProperty.possible_extensions_for_arch(arch)
    assert set(exts) == set(expect)


@pytest.mark.parametrize(
    "init_data, expect_str, expect_fmt, expect_flat, expect_last, expect_bool",
    (
        (
            [
                (1, [Version("1.1.1"), Version("1.1.2")]),
                (2, [Version("1.2.1"), Version("1.2.2")]),
            ],
            "[[Version('1.1.1'), Version('1.1.2')], [Version('1.2.1'), Version('1.2.2')]]",
            "1.1.1 1.1.2\n1.2.1 1.2.2",
            [Version("1.1.1"), Version("1.1.2"), Version("1.2.1"), Version("1.2.2")],
            Version("1.2.2"),
            True,
        ),
        (
            [],
            "[]",
            "",
            [],
            None,
            False,
        ),
        (
            Version("1.2.3"),
            "[[Version('1.2.3')]]",
            "1.2.3",
            [Version("1.2.3")],
            Version("1.2.3"),
            True,
        ),
    ),
)
def test_versions(init_data, expect_str, expect_fmt, expect_flat, expect_last, expect_bool):
    versions = Versions(init_data)
    assert str(versions) == expect_str
    assert format(versions) == expect_fmt
    assert format(versions, "s") == expect_str
    assert versions.flattened() == expect_flat
    assert versions.latest() == expect_last
    assert bool(versions) == expect_bool

    with pytest.raises(TypeError) as pytest_wrapped_e:
        format(versions, "x")
    assert pytest_wrapped_e.type == TypeError


@pytest.fixture
def spec_regex():
    return re.compile(r"^(\d+\.\d+)")


@pytest.mark.parametrize(
    "os_name,target,in_file,expect_out_file",
    [
        ("windows", "android", "windows-android.html", "windows-android-expect.json"),
        ("windows", "desktop", "windows-desktop.html", "windows-desktop-expect.json"),
        ("windows", "winrt", "windows-winrt.html", "windows-winrt-expect.json"),
        ("linux", "android", "linux-android.html", "linux-android-expect.json"),
        ("linux", "desktop", "linux-desktop.html", "linux-desktop-expect.json"),
        ("mac", "android", "mac-android.html", "mac-android-expect.json"),
        ("mac", "desktop", "mac-desktop.html", "mac-desktop-expect.json"),
        ("mac", "ios", "mac-ios.html", "mac-ios-expect.json"),
    ],
)
def test_list_versions_tools(monkeypatch, spec_regex, os_name, target, in_file, expect_out_file):
    _html = (Path(__file__).parent / "data" / in_file).read_text("utf-8")
    monkeypatch.setattr(MetadataFactory, "fetch_http", lambda self, _: _html)

    expected = json.loads((Path(__file__).parent / "data" / expect_out_file).read_text("utf-8"))

    # Test 'aqt list-tool'
    tools = MetadataFactory(ArchiveId("tools", os_name, target)).getList()
    assert tools == expected["tools"]

    for ext, expected_output in expected["qt"].items():
        # Test 'aqt list-qt'
        archive_id = ArchiveId("qt", os_name, target, ext if ext != "qt" else "")
        all_versions = MetadataFactory(archive_id).getList()

        if len(expected_output) == 0:
            assert not all_versions
        else:
            assert f"{all_versions}" == "\n".join(expected_output)

        # Filter for the latest version only
        latest_ver = MetadataFactory(archive_id, is_latest_version=True).getList()

        if len(expected_output) == 0:
            assert not latest_ver
        else:
            assert f"{latest_ver}" == expected_output[-1].split(" ")[-1]

        for row in expected_output:
            spec_str = spec_regex.search(row).group(1)
            spec = SimpleSpec(spec_str) if not ext.endswith("preview") else SimpleSpec(spec_str + ".0-preview")

            # Find the latest version for a particular spec
            latest_ver_for_spec = MetadataFactory(
                archive_id,
                spec=spec,
                is_latest_version=True,
            ).getList()
            assert f"{latest_ver_for_spec}" == row.split(" ")[-1]

            # Find all versions for a particular spec
            all_ver_for_spec = MetadataFactory(
                archive_id,
                spec=spec,
            ).getList()
            assert f"{all_ver_for_spec}" == row


@pytest.mark.parametrize(
    "version,extension,in_file,expect_out_file",
    [
        ("5.14.0", "", "windows-5140-update.xml", "windows-5140-expect.json"),
        ("5.15.0", "", "windows-5150-update.xml", "windows-5150-expect.json"),
        (
            "5.15.2",
            "src_doc_examples",
            "windows-5152-src-doc-example-update.xml",
            "windows-5152-src-doc-example-expect.json",
        ),
        ("6.2.0", "", "windows-620-update.xml", "windows-620-expect.json"),
    ],
)
def test_list_architectures_and_modules(monkeypatch, version: str, extension: str, in_file: str, expect_out_file: str):
    archive_id = ArchiveId("qt", "windows", "desktop", extension)
    _xml = (Path(__file__).parent / "data" / in_file).read_text("utf-8")
    expect = json.loads((Path(__file__).parent / "data" / expect_out_file).read_text("utf-8"))

    monkeypatch.setattr(MetadataFactory, "fetch_http", lambda self, _: _xml)

    for arch in expect["architectures"]:
        modules = MetadataFactory(archive_id).fetch_modules(Version(version), arch)
        assert modules == sorted(expect["modules_by_arch"][arch])

    arches = MetadataFactory(archive_id).fetch_arches(Version(version))
    assert arches == expect["architectures"]


@pytest.mark.parametrize(
    "version, arch, modules_to_query, modules_failed_query",
    (
        ("5.14.0", "win32_mingw73", [], []),
        ("5.14.0", "win32_mingw73", ["qtcharts"], []),
        ("5.14.0", "win32_mingw73", ["all"], []),
        ("5.14.0", "win32_mingw73", ["debug_info"], ["debug_info"]),
        ("5.14.0", "win64_msvc2017_64", [], []),
        ("5.14.0", "win64_msvc2017_64", ["debug_info"], []),
    ),
)
def test_list_archives(
    monkeypatch, capsys, version: str, arch: str, modules_to_query: List[str], modules_failed_query: List[str]
):
    archive_id = ArchiveId("qt", "windows", "desktop")
    in_file = f"{archive_id.host}-{version.replace('.', '')}-update.xml"
    expect_out_file = f"{archive_id.host}-{version.replace('.', '')}-expect.json"
    _xml = (Path(__file__).parent / "data" / in_file).read_text("utf-8")
    monkeypatch.setattr(MetadataFactory, "fetch_http", lambda self, _: _xml)
    expect = json.loads((Path(__file__).parent / "data" / expect_out_file).read_text("utf-8"))

    if not modules_to_query:
        expected_qt_archives = expect["qt_base_pkgs_by_arch"][arch]["DownloadableArchives"]
        expected = set([arc.split("-")[0] for arc in expected_qt_archives])
    else:
        expected_mod_metadata = expect["modules_metadata_by_arch"][arch]
        if "all" not in modules_to_query:
            expected_mod_metadata = filter(lambda mod: mod["Name"].split(".")[-2] in modules_to_query, expected_mod_metadata)
        expected = set([arc.split("-")[0] for mod in expected_mod_metadata for arc in mod["DownloadableArchives"]])

    archives_query = [version, arch, *modules_to_query]
    cli_args = ["list-qt", "windows", "desktop", "--archives", *archives_query]
    if not modules_failed_query:
        meta = set(MetadataFactory(archive_id, archives_query=archives_query).getList())
        assert meta == expected

        cli = Cli()
        assert 0 == cli.run(cli_args)
        out, err = capsys.readouterr()
        assert out.rstrip() == " ".join(sorted(expected))
        return

    expected_err_msg = f"The requested modules were not located: {sorted(modules_failed_query)}"
    with pytest.raises(CliInputError) as err:
        MetadataFactory(archive_id, archives_query=archives_query).getList()
    assert err.type == CliInputError
    assert format(err.value).startswith(expected_err_msg)

    cli = Cli()
    assert 1 == cli.run(cli_args)
    out, err = capsys.readouterr()
    assert expected_err_msg in err


def test_list_archives_insufficient_args(capsys):
    cli = Cli()
    assert 1 == cli.run("list-qt mac desktop --archives 5.14.0".split())
    out, err = capsys.readouterr()
    assert err.strip() == "The '--archives' flag requires a 'QT_VERSION' and an 'ARCHITECTURE' parameter."


def test_list_archives_bad_xml(monkeypatch):
    archive_id = ArchiveId("qt", "windows", "desktop")
    archives_query = ["5.15.2", "win32_mingw81", "qtcharts"]

    xml_no_name = "<Updates><PackageUpdate><badname></badname></PackageUpdate></Updates>"
    xml_empty_name = "<Updates><PackageUpdate><Name></Name></PackageUpdate></Updates>"
    xml_broken = "<Updates></PackageUpdate><PackageUpdate></Updates><Name></Name>"

    for _xml in (xml_no_name, xml_empty_name, xml_broken):
        monkeypatch.setattr(MetadataFactory, "fetch_http", lambda self, _: _xml)
        with pytest.raises(ArchiveListError) as e:
            MetadataFactory(archive_id, archives_query=archives_query).getList()
        assert e.type == ArchiveListError


@pytest.mark.parametrize(
    "host, target, tool_name",
    [
        ("mac", "desktop", "tools_cmake"),
        ("mac", "desktop", "tools_ifw"),
        ("mac", "desktop", "tools_qtcreator"),
    ],
)
def test_tool_modules(monkeypatch, host: str, target: str, tool_name: str):
    archive_id = ArchiveId("tools", host, target)
    in_file = "{}-{}-{}-update.xml".format(host, target, tool_name)
    expect_out_file = "{}-{}-{}-expect.json".format(host, target, tool_name)
    _xml = (Path(__file__).parent / "data" / in_file).read_text("utf-8")
    expect = json.loads((Path(__file__).parent / "data" / expect_out_file).read_text("utf-8"))

    monkeypatch.setattr(MetadataFactory, "fetch_http", lambda self, _: _xml)

    modules = MetadataFactory(archive_id, tool_name=tool_name).getList()
    assert modules == expect["modules"]

    table = MetadataFactory(archive_id, tool_name=tool_name, is_long_listing=True).getList()
    assert table._rows() == expect["long_listing"]


@pytest.fixture
def expected_windows_desktop_5140() -> Dict:
    xmlexpect = "windows-5140-expect.json"
    return json.loads((Path(__file__).parent / "data" / xmlexpect).read_text("utf-8"))


@pytest.mark.parametrize(
    "args, expect",
    (
        ("--extensions latest", {"src_doc_examples"}),
        ("--spec 5.14 --extensions latest", {"wasm", "src_doc_examples"}),
        ("--extensions 5.14.0", {"wasm", "src_doc_examples"}),
        ("--modules latest win64_msvc2017_64", ["modules_by_arch", "win64_msvc2017_64"]),
        ("--spec 5.14 --modules latest win64_msvc2017_64", ["modules_by_arch", "win64_msvc2017_64"]),
        ("--modules 5.14.0 win32_mingw73", ["modules_by_arch", "win32_mingw73"]),
        ("--modules 5.14.0 win32_msvc2017", ["modules_by_arch", "win32_msvc2017"]),
        ("--modules 5.14.0 win64_mingw73", ["modules_by_arch", "win64_mingw73"]),
        ("--modules 5.14.0 win64_msvc2015_64", ["modules_by_arch", "win64_msvc2015_64"]),
        ("--modules 5.14.0 win64_msvc2017_64", ["modules_by_arch", "win64_msvc2017_64"]),
        ("--extension wasm --modules latest win64_msvc2017_64", ["modules_by_arch", "win64_msvc2017_64"]),
        ("--extension wasm --spec 5.14 --modules latest win64_msvc2017_64", ["modules_by_arch", "win64_msvc2017_64"]),
        ("--extension wasm --modules 5.14.0 win64_msvc2017_64", ["modules_by_arch", "win64_msvc2017_64"]),
        ("--arch latest", ["architectures"]),
        ("--spec 5.14 --arch latest", ["architectures"]),
        ("--arch 5.14.0", ["architectures"]),
        ("--extension wasm --arch latest", ["architectures"]),
        ("--extension wasm --spec 5.14 --arch latest", ["architectures"]),
        ("--extension wasm --arch 5.14.0", ["architectures"]),
    ),
)
def test_list_qt_cli(
    monkeypatch,
    capsys,
    expected_windows_desktop_5140: Dict[str, Set[str]],
    args: str,
    expect: Union[Set[str], List[str]],
):
    htmlfile, xmlfile = "windows-desktop.html", "windows-5140-update.xml"
    version_string_to_replace = "qt5.5140"
    if isinstance(expect, list):
        # In this case, 'expect' is a list of keys to follow to the expected values.
        expected_dict = expected_windows_desktop_5140
        for key in expect:  # Follow the chain of keys to the list of values.
            expected_dict = expected_dict[key]
        assert isinstance(expected_dict, list)
        expect_set = set(expected_dict)
    else:
        expect_set = expect
    assert isinstance(expect_set, set)

    def _mock_fetch_http(_, rest_of_url: str) -> str:
        htmltext = (Path(__file__).parent / "data" / htmlfile).read_text("utf-8")
        if not rest_of_url.endswith("Updates.xml"):
            return htmltext

        xmltext = (Path(__file__).parent / "data" / xmlfile).read_text("utf-8")
        # If we are serving an Updates.xml, `aqt list` will look for a Qt version number.
        # We will replace the version numbers in the file with the requested version.
        match = re.search(r"qt(\d)_(\d+)", rest_of_url)
        assert match
        major, version_nodot = match.groups()
        desired_version_string = f"qt{major}.{version_nodot}"
        return xmltext.replace(version_string_to_replace, desired_version_string)

    monkeypatch.setattr(MetadataFactory, "fetch_http", _mock_fetch_http)

    cli = Cli()
    cli.run(["list-qt", "windows", "desktop", *args.split()])
    out, err = capsys.readouterr()
    output_set = set(out.strip().split())
    assert output_set == expect_set


@pytest.mark.parametrize(
    "cmd, host, expect",
    (
        ("list-qt", "windows", {"desktop", "android", "winrt"}),
        ("list-qt", "linux", {"desktop", "android"}),
        ("list-qt", "mac", {"desktop", "android", "ios"}),
        ("list-tool", "windows", {"desktop", "android", "winrt"}),
        ("list-tool", "linux", {"desktop", "android"}),
        ("list-tool", "mac", {"desktop", "android", "ios"}),
    ),
)
def test_list_targets(capsys, cmd: str, host: str, expect: Set[str]):
    cli = Cli()
    cli.run([cmd, host])
    out, err = capsys.readouterr()
    output_set = set(out.strip().split())
    assert output_set == expect


@pytest.mark.parametrize(
    "cmd, host, target",
    (
        ("list-qt", "windows", "ios"),
        ("list-qt", "linux", "ios"),
        ("list-qt", "linux", "winrt"),
        ("list-qt", "mac", "winrt"),
        ("list-tool", "windows", "ios"),
        ("list-tool", "linux", "ios"),
        ("list-tool", "linux", "winrt"),
        ("list-tool", "mac", "winrt"),
    ),
)
def test_list_wrong_target(capsys, cmd: str, host: str, target: str):
    expect = f"'{target}' is not a valid target for host '{host}'"

    cli = Cli()
    return_code = cli.run([cmd, host, target])
    out, err = capsys.readouterr()
    assert return_code == 1
    assert err.strip() == expect


@pytest.mark.parametrize(
    "cmd, spec",
    (
        ("list-qt", "not a spec"),
        ("list-qt", "1...3"),
        ("list-qt", ""),
        ("list-qt", ">3 <5"),
    ),
)
def test_invalid_spec(capsys, cmd: str, spec: str):
    expect_prefix = f"Invalid version specification: '{spec}'"
    host, target = "linux", "desktop"
    cli = Cli()
    return_code = cli.run([cmd, host, target, "--spec", spec])
    out, err = capsys.readouterr()
    assert return_code == 1
    assert err.strip().startswith(expect_prefix)


@pytest.mark.parametrize(
    "simple_spec, expected_name",
    (
        (SimpleSpec("*"), "mytool.999"),
        (SimpleSpec(">3.5"), "mytool.999"),
        (SimpleSpec("3.5.5"), "mytool.355"),
        (SimpleSpec("<3.5"), "mytool.300"),
        (SimpleSpec("<=3.5"), "mytool.355"),
        (SimpleSpec("<=3.5.0"), "mytool.350"),
        (SimpleSpec(">10"), None),
    ),
)
def test_list_choose_tool_by_version(simple_spec, expected_name):
    tools_data = {
        "mytool.999": {"Version": "9.9.9", "Name": "mytool.999"},
        "mytool.355": {"Version": "3.5.5", "Name": "mytool.355"},
        "mytool.350": {"Version": "3.5.0", "Name": "mytool.350"},
        "mytool.300": {"Version": "3.0.0", "Name": "mytool.300"},
    }
    item = MetadataFactory.choose_highest_version_in_spec(tools_data, simple_spec)
    if item is not None:
        assert item["Name"] == expected_name
    else:
        assert expected_name is None


qt6_android_requires_ext_msg = (
    "Qt 6 for Android requires one of the following extensions: "
    f"{ArchiveId.EXTENSIONS_REQUIRED_ANDROID_QT6}. "
    "Please add your extension using the `--extension` flag."
)
no_arm64_v8_msg = "The extension 'arm64_v8a' is only valid for Qt 6 for Android"
no_wasm_msg = "The extension 'wasm' is only available in Qt 5.13-5.15 and 6.2+ on desktop."


@pytest.mark.parametrize(
    "target, ext, version, expected_msg",
    (
        ("android", "", "6.2.0", qt6_android_requires_ext_msg),
        ("android", "arm64_v8a", "5.13.0", no_arm64_v8_msg),
        ("desktop", "arm64_v8a", "5.13.0", no_arm64_v8_msg),
        ("desktop", "arm64_v8a", "6.2.0", no_arm64_v8_msg),
        ("desktop", "wasm", "5.12.11", no_wasm_msg),  # out of range
        ("desktop", "wasm", "6.1.9", no_wasm_msg),  # out of range
        ("android", "wasm", "5.12.11", no_wasm_msg),  # in range, wrong target
        ("android", "wasm", "5.14.0", no_wasm_msg),  # in range, wrong target
        ("android", "wasm", "6.1.9", qt6_android_requires_ext_msg),
    ),
)
def test_list_invalid_extensions(capsys, monkeypatch, target, ext, version, expected_msg):
    host = "windows"
    extension_params = ["--extension", ext] if ext else []
    cli = Cli()
    cli.run(["list-qt", host, target, *extension_params, "--arch", version])
    out, err = capsys.readouterr()
    sys.stdout.write(out)
    sys.stderr.write(err)
    assert expected_msg in err


mac_qt = ArchiveId("qt", "mac", "desktop")
mac_wasm = ArchiveId("qt", "mac", "desktop", "wasm")
wrong_tool_name_msg = "Please use 'aqt list-tool mac desktop' to check what tools are available."
wrong_qt_version_msg = "Please use 'aqt list-qt mac desktop' to show versions of Qt available."
wrong_ext_msg = "Please use 'aqt list-qt mac desktop --extensions <QT_VERSION>' to list valid extensions."
wrong_arch_msg = "Please use 'aqt list-qt mac desktop --arch <QT_VERSION>' to list valid architectures."


@pytest.mark.parametrize(
    "meta, expected_message",
    (
        (MetadataFactory(mac_qt), []),
        (
            MetadataFactory(mac_qt, spec=SimpleSpec("5.0")),
            ["Please use 'aqt list-qt mac desktop' to check that versions of qt exist within the spec '5.0'."],
        ),
        (
            MetadataFactory(ArchiveId("tools", "mac", "desktop"), tool_name="ifw"),
            [wrong_tool_name_msg],
        ),
        (
            MetadataFactory(mac_qt, architectures_ver="1.2.3"),
            [wrong_qt_version_msg],
        ),
        (
            MetadataFactory(mac_qt, modules_query=("1.2.3", "clang_64")),
            [wrong_qt_version_msg, wrong_arch_msg],
        ),
        (
            MetadataFactory(mac_qt, extensions_ver="1.2.3"),
            [wrong_qt_version_msg],
        ),
        (
            MetadataFactory(mac_qt, archives_query=["1.2.3", "clang_64", "a module", "another module"]),
            [
                "Please use 'aqt list-qt mac desktop' to show versions of Qt available.",
                "Please use 'aqt list-qt mac desktop --arch <QT_VERSION>' to show architectures available.",
                "Please use 'aqt list-qt mac desktop --modules <QT_VERSION>' to show modules available.",
            ],
        ),
        (
            MetadataFactory(mac_qt, archives_query=["1.2.3", "clang_64"]),
            [
                "Please use 'aqt list-qt mac desktop' to show versions of Qt available.",
                "Please use 'aqt list-qt mac desktop --arch <QT_VERSION>' to show architectures available.",
            ],
        ),
        (
            MetadataFactory(mac_wasm),
            [wrong_ext_msg],
        ),
        (
            MetadataFactory(mac_wasm, spec=SimpleSpec("<5.9")),
            [
                wrong_ext_msg,
                "Please use 'aqt list-qt mac desktop' to check that versions of qt exist within the spec '<5.9'.",
            ],
        ),
        (
            MetadataFactory(ArchiveId("tools", "mac", "desktop", "wasm"), tool_name="ifw"),
            [
                "Please use 'aqt list-tool mac desktop --extensions <QT_VERSION>' to list valid extensions.",
                wrong_tool_name_msg,
            ],
        ),
        (
            MetadataFactory(mac_wasm, architectures_ver="1.2.3"),
            [wrong_ext_msg, wrong_qt_version_msg],
        ),
        (
            MetadataFactory(mac_wasm, modules_query=("1.2.3", "clang_64")),
            [wrong_ext_msg, wrong_qt_version_msg, wrong_arch_msg],
        ),
        (
            MetadataFactory(mac_wasm, extensions_ver="1.2.3"),
            [wrong_ext_msg, wrong_qt_version_msg],
        ),
    ),
)
def test_suggested_follow_up(meta: MetadataFactory, expected_message: str):
    assert suggested_follow_up(meta) == expected_message


def test_format_suggested_follow_up():
    suggestions = [
        "Please use 'aqt list-tool mac desktop --extensions <QT_VERSION>' to list valid extensions.",
        "Please use 'aqt list-tool mac desktop' to check what tools are available.",
    ]
    expected = (
        "==============================Suggested follow-up:==============================\n"
        "* Please use 'aqt list-tool mac desktop --extensions <QT_VERSION>' to list valid extensions.\n"
        "* Please use 'aqt list-tool mac desktop' to check what tools are available."
    )
    ex = AqtException("msg", suggested_action=suggestions)
    assert ex._format_suggested_follow_up() == expected


def test_format_suggested_follow_up_empty():
    ex = AqtException("msg", suggested_action=[])
    assert format(ex) == "msg"


@pytest.mark.parametrize(
    "meta, expect",
    (
        (
            MetadataFactory(ArchiveId("qt", "mac", "desktop"), spec=SimpleSpec("5.42")),
            "qt/mac/desktop with spec 5.42",
        ),
        (
            MetadataFactory(ArchiveId("qt", "mac", "desktop", "wasm"), spec=SimpleSpec("5.42")),
            "qt/mac/desktop/wasm with spec 5.42",
        ),
        (MetadataFactory(ArchiveId("qt", "mac", "desktop")), "qt/mac/desktop"),
        (
            MetadataFactory(ArchiveId("qt", "mac", "desktop", "wasm")),
            "qt/mac/desktop/wasm",
        ),
    ),
)
def test_list_describe_filters(meta: MetadataFactory, expect: str):
    assert meta.describe_filters() == expect


@pytest.mark.parametrize(
    "archive_id, spec, version_str, expect",
    (
        (mac_qt, None, "5.12.42", Version("5.12.42")),
        (
            mac_qt,
            None,
            "not a version",
            CliInputError("Invalid version string: 'not a version'"),
        ),
        (mac_qt, SimpleSpec("5"), "latest", Version("5.15.2")),
        (
            mac_qt,
            SimpleSpec("5.0"),
            "latest",
            CliInputError("There is no latest version of Qt with the criteria 'qt/mac/desktop with spec 5.0'"),
        ),
    ),
)
def test_list_to_version(monkeypatch, archive_id, spec, version_str, expect):
    _html = (Path(__file__).parent / "data" / "mac-desktop.html").read_text("utf-8")
    monkeypatch.setattr(MetadataFactory, "fetch_http", lambda self, _: _html)

    if isinstance(expect, Exception):
        with pytest.raises(CliInputError) as error:
            MetadataFactory(archive_id, spec=spec)._to_version(version_str)
        assert error.type == CliInputError
        assert str(expect) == str(error.value)
    else:
        assert MetadataFactory(archive_id, spec=spec)._to_version(version_str) == expect


def test_list_fetch_tool_by_simple_spec(monkeypatch):
    update_xml = (Path(__file__).parent / "data" / "windows-desktop-tools_vcredist-update.xml").read_text("utf-8")
    monkeypatch.setattr(MetadataFactory, "fetch_http", lambda self, _: update_xml)

    expect_json = (Path(__file__).parent / "data" / "windows-desktop-tools_vcredist-expect.json").read_text("utf-8")
    expected = json.loads(expect_json)["modules_data"]

    def check(actual, expect):
        for key in (
            "Description",
            "DisplayName",
            "DownloadableArchives",
            "ReleaseDate",
            "SHA1",
            "Version",
            "Virtual",
        ):
            assert actual[key] == expect[key]

    meta = MetadataFactory(ArchiveId("tools", "windows", "desktop"))
    check(
        meta.fetch_tool_by_simple_spec(tool_name="tools_vcredist", simple_spec=SimpleSpec("2011")),
        expected["qt.tools.vcredist"],
    )
    check(
        meta.fetch_tool_by_simple_spec(tool_name="tools_vcredist", simple_spec=SimpleSpec("2014")),
        expected["qt.tools.vcredist_msvc2013_x86"],
    )
    nonexistent = meta.fetch_tool_by_simple_spec(tool_name="tools_vcredist", simple_spec=SimpleSpec("1970"))
    assert nonexistent is None

    # Simulate a broken Updates.xml file, with invalid versions
    highest_module_info = MetadataFactory.choose_highest_version_in_spec(
        all_tools_data={"some_module": {"Version": "not_a_version"}},
        simple_spec=SimpleSpec("*"),
    )
    assert highest_module_info is None


@pytest.mark.parametrize(
    "columns, expect",
    (
        (
            120,
            (
                "Tool Variant Name        Version         Release Date          Display Name          "
                "            Description            \n"
                "====================================================================================="
                "===================================\n"
                "qt.tools.ifw.41     4.1.1-202105261132   2021-05-26     Qt Installer Framework 4.1   "
                "The Qt Installer Framework provides\n"
                "                                                                                     "
                "a set of tools and utilities to    \n"
                "                                                                                     "
                "create installers for the supported\n"
                "                                                                                     "
                "desktop Qt platforms: Linux,       \n"
                "                                                                                     "
                "Microsoft Windows, and macOS.      \n"
            ),
        ),
        (
            80,
            "Tool Variant Name        Version         Release Date\n"
            "=====================================================\n"
            "qt.tools.ifw.41     4.1.1-202105261132   2021-05-26  \n",
        ),
        (
            0,
            "Tool Variant Name        Version         Release Date          Display Name          "
            "                                                                           Descriptio"
            "n                                                                            \n"
            "====================================================================================="
            "====================================================================================="
            "=============================================================================\n"
            "qt.tools.ifw.41     4.1.1-202105261132   2021-05-26     Qt Installer Framework 4.1   "
            "The Qt Installer Framework provides a set of tools and utilities to create installers"
            " for the supported desktop Qt platforms: Linux, Microsoft Windows, and macOS.\n",
        ),
    ),
)
def test_show_list_tools_long_ifw(capsys, monkeypatch, columns, expect):
    update_xml = (Path(__file__).parent / "data" / "mac-desktop-tools_ifw-update.xml").read_text("utf-8")
    monkeypatch.setattr(MetadataFactory, "fetch_http", lambda self, _: update_xml)

    monkeypatch.setattr(shutil, "get_terminal_size", lambda fallback: os.terminal_size((columns, 24)))

    meta = MetadataFactory(
        ArchiveId("tools", "mac", "desktop"),
        tool_name="tools_ifw",
        is_long_listing=True,
    )
    show_list(meta)
    out, err = capsys.readouterr()
    sys.stdout.write(out)
    sys.stderr.write(err)
    assert out == expect


def test_show_list_versions(monkeypatch, capsys):
    _html = (Path(__file__).parent / "data" / "mac-desktop.html").read_text("utf-8")
    monkeypatch.setattr(MetadataFactory, "fetch_http", lambda *args: _html)

    expect_file = Path(__file__).parent / "data" / "mac-desktop-expect.json"
    expected = "\n".join(json.loads(expect_file.read_text("utf-8"))["qt"]["qt"]) + "\n"

    show_list(MetadataFactory(mac_qt))
    out, err = capsys.readouterr()
    assert out == expected


def test_show_list_tools(monkeypatch, capsys):
    page = (Path(__file__).parent / "data" / "mac-desktop.html").read_text("utf-8")
    monkeypatch.setattr(MetadataFactory, "fetch_http", lambda self, _: page)

    expect_file = Path(__file__).parent / "data" / "mac-desktop-expect.json"
    expect = "\n".join(json.loads(expect_file.read_text("utf-8"))["tools"]) + "\n"

    meta = MetadataFactory(ArchiveId("tools", "mac", "desktop"))
    show_list(meta)
    out, err = capsys.readouterr()
    sys.stdout.write(out)
    sys.stderr.write(err)
    assert out == expect


def test_show_list_empty(monkeypatch, capsys):
    monkeypatch.setattr(MetadataFactory, "getList", lambda self: [])
    meta = MetadataFactory(ArchiveId("tools", "mac", "desktop"))
    with pytest.raises(EmptyMetadata) as error:
        show_list(meta)
    assert error.type == EmptyMetadata
    assert format(error.value) == "No tools available for this request."


def test_show_list_bad_connection(monkeypatch, capsys):
    for exception_class, error_msg in (
        (ArchiveConnectionError, "Failure to connect to some url"),
        (ArchiveDownloadError, "Failure to download some xml file"),
    ):

        def mock(self):
            raise exception_class(error_msg)

        monkeypatch.setattr(MetadataFactory, "getList", mock)
        meta = MetadataFactory(mac_wasm, spec=SimpleSpec("<5.9"))
        with pytest.raises(exception_class) as error:
            show_list(meta)
        assert error.type == exception_class
        assert format(error.value) == (
            f"{error_msg}\n"
            "==============================Suggested follow-up:==============================\n"
            "* Please use 'aqt list-qt mac desktop --extensions <QT_VERSION>' to list valid extensions.\n"
            "* Please use 'aqt list-qt mac desktop' to check that versions of qt exist within the spec '<5.9'."
        )


def fetch_expected_tooldata(json_filename: str) -> ToolData:
    text = (Path(__file__).parent / "data" / json_filename).read_text("utf-8")
    raw_tooldata: List[List[str]] = json.loads(text)["long_listing"]
    keys = ("Version", "ReleaseDate", "DisplayName", "Description")

    tools: Dict[str, Dict[str, str]] = {}
    for variant_name, *values in raw_tooldata:
        assert len(keys) == len(values)
        tools[variant_name] = {k: v for k, v in zip(keys, values)}

    return ToolData(tools)


@pytest.mark.parametrize("host, target, tool_name", (("mac", "desktop", "tools_cmake"),))
def test_list_tool_cli(monkeypatch, capsys, host: str, target: str, tool_name: str):
    html_file = f"{host}-{target}.html"
    xml_file = f"{host}-{target}-{tool_name}-update.xml"
    html_expect = f"{host}-{target}-expect.json"
    xml_expect = f"{host}-{target}-{tool_name}-expect.json"
    htmltext, xmltext, htmljson, xmljson = [
        (Path(__file__).parent / "data" / filename).read_text("utf-8")
        for filename in (html_file, xml_file, html_expect, xml_expect)
    ]
    expected_tools = set(json.loads(htmljson)["tools"])
    xml_data = json.loads(xmljson)
    expected_tool_modules = set(xml_data["modules"])

    def _mock_fetch_http(_, rest_of_url: str) -> str:
        if not rest_of_url.endswith("Updates.xml"):
            return htmltext
        folder = urlparse(rest_of_url).path.split("/")[-2]
        assert folder.startswith("tools_")
        return xmltext

    monkeypatch.setattr(MetadataFactory, "fetch_http", _mock_fetch_http)

    cli = Cli()
    cli.run(["list-tool", host, target])
    out, err = capsys.readouterr()
    output_set = set(out.strip().split())
    assert output_set == expected_tools

    cli.run(["list-tool", host, target, tool_name])
    out, err = capsys.readouterr()
    output_set = set(out.strip().split())
    assert output_set == expected_tool_modules

    # Test abbreviated tool name: "aqt list-tool mac desktop ifw"
    assert tool_name.startswith("tools_")
    short_tool_name = tool_name[6:]
    cli.run(["list-tool", host, target, short_tool_name])
    out, err = capsys.readouterr()
    output_set = set(out.strip().split())
    assert output_set == expected_tool_modules

    cli.run(["list-tool", host, target, tool_name, "-l"])
    out, err = capsys.readouterr()

    expected_tooldata = format(fetch_expected_tooldata(xml_expect))
    assert out.strip() == expected_tooldata


def test_fetch_http_ok(monkeypatch):
    monkeypatch.setattr("aqt.metadata.getUrl", lambda **kwargs: "some_html_content")
    assert MetadataFactory.fetch_http("some_url") == "some_html_content"


def test_fetch_http_failover(monkeypatch):
    urls_requested = set()

    def _mock(url, **kwargs):
        urls_requested.add(url)
        if len(urls_requested) <= 1:
            raise ArchiveDownloadError()
        return "some_html_content"

    monkeypatch.setattr("aqt.metadata.getUrl", _mock)

    # Require that the first attempt failed, but the second did not
    assert MetadataFactory.fetch_http("some_url") == "some_html_content"
    assert len(urls_requested) == 2


def test_fetch_http_download_error(monkeypatch):
    urls_requested = set()

    def _mock(url, **kwargs):
        urls_requested.add(url)
        raise ArchiveDownloadError()

    monkeypatch.setattr("aqt.metadata.getUrl", _mock)
    with pytest.raises(ArchiveDownloadError) as e:
        MetadataFactory.fetch_http("some_url")
    assert e.type == ArchiveDownloadError

    # Require that a fallback url was tried
    assert len(urls_requested) == 2


def test_fetch_http_conn_error(monkeypatch):
    urls_requested = set()

    def _mock(url, **kwargs):
        urls_requested.add(url)
        raise ArchiveConnectionError()

    monkeypatch.setattr("aqt.metadata.getUrl", _mock)
    with pytest.raises(ArchiveConnectionError) as e:
        MetadataFactory.fetch_http("some_url")
    assert e.type == ArchiveConnectionError

    # Require that a fallback url was tried
    assert len(urls_requested) == 2
