import os
import typing
from collections.abc import Mapping

import pytest
from debian.deb822 import Deb822
from debian.debian_support import DpkgArchTable

from debputy._deb_options_profiles import DebBuildOptionsAndProfiles
from debputy.architecture_support import DpkgArchitectureBuildProcessValuesTable
from debputy.exceptions import PureVirtualPathError
from debputy.filesystem_scan import OSFSROOverlay
from debputy.highlevel_manifest_parser import YAMLManifestParser
from debputy.manifest_parser.util import AttributePath
from debputy.packages import BinaryPackage, SourcePackage, DctrlParser
from debputy.plugin.api import VirtualPath
from debputy.plugin.api.feature_set import PluginProvidedFeatureSet
from debputy.plugin.api.impl import (
    DebputyPluginInitializerProvider,
    _resolve_bundled_plugin_docs_path,
    find_json_plugin,
)
from debputy.plugin.api.impl_types import (
    DebputyPluginMetadata,
)
from debputy.plugins.debputy.debputy_plugin import initialize_debputy_features
from debputy.substitution import (
    NULL_SUBSTITUTION,
    Substitution,
    SubstitutionImpl,
    VariableContext,
)
from debputy.util import assume_not_none
from debputy.version import DEBPUTY_PLUGIN_ROOT_DIR

# Disable dpkg's translation layer.  It is very slow and disabling it makes it easier to debug
# test-failure reports from systems with translations active.
os.environ["DPKG_NLS"] = "0"


@pytest.fixture(scope="session")
def amd64_dpkg_architecture_variables() -> DpkgArchitectureBuildProcessValuesTable:
    return DpkgArchitectureBuildProcessValuesTable(fake_host="amd64")


@pytest.fixture(scope="session")
def dpkg_arch_query() -> DpkgArchTable:
    return DpkgArchTable.load_arch_table()


@pytest.fixture()
def source_package() -> SourcePackage:
    return SourcePackage(
        {
            "Source": "foo",
        }
    )


@pytest.fixture()
def package_single_foo_arch_all_cxt_amd64(
    amd64_dpkg_architecture_variables,
    dpkg_arch_query,
) -> Mapping[str, BinaryPackage]:
    return {
        p.name: p
        for p in [
            BinaryPackage(
                Deb822(
                    {
                        "Package": "foo",
                        "Architecture": "all",
                    }
                ),
                amd64_dpkg_architecture_variables,
                dpkg_arch_query,
                is_main_package=True,
            )
        ]
    }


@pytest.fixture()
def package_foo_w_udeb_arch_any_cxt_amd64(
    amd64_dpkg_architecture_variables,
    dpkg_arch_query,
) -> Mapping[str, BinaryPackage]:
    return {
        p.name: p
        for p in [
            BinaryPackage(
                Deb822(
                    {
                        "Package": "foo",
                        "Architecture": "any",
                    }
                ),
                amd64_dpkg_architecture_variables,
                dpkg_arch_query,
                is_main_package=True,
            ),
            BinaryPackage(
                Deb822(
                    {
                        "Package": "foo-udeb",
                        "Architecture": "any",
                        "Package-Type": "udeb",
                    }
                ),
                amd64_dpkg_architecture_variables,
                dpkg_arch_query,
            ),
        ]
    }


@pytest.fixture(scope="session")
def null_substitution() -> Substitution:
    return NULL_SUBSTITUTION


@pytest.fixture(scope="function")
def _empty_debputy_plugin_feature_set() -> PluginProvidedFeatureSet:
    return PluginProvidedFeatureSet()


@pytest.fixture(scope="function")
def amd64_substitution(
    amd64_dpkg_architecture_variables,
    _empty_debputy_plugin_feature_set,
) -> Substitution:
    debian_dir = OSFSROOverlay.create_root_dir("debian", "debian")
    variable_context = VariableContext(
        debian_dir,
    )
    return SubstitutionImpl(
        plugin_feature_set=_empty_debputy_plugin_feature_set,
        dpkg_arch_table=amd64_dpkg_architecture_variables,
        static_variables=None,
        environment={},
        unresolvable_substitutions=frozenset(["SOURCE_DATE_EPOCH", "PACKAGE"]),
        variable_context=variable_context,
    )


@pytest.fixture(scope="session")
def no_profiles_or_build_options() -> DebBuildOptionsAndProfiles:
    return DebBuildOptionsAndProfiles(environ={})


@pytest.fixture(scope="function")
def debputy_plugin_feature_set(
    request,
    _empty_debputy_plugin_feature_set,
    amd64_substitution,
) -> PluginProvidedFeatureSet:
    plugin_name = "debputy"
    debputy_plugin_metadata = DebputyPluginMetadata(
        plugin_name=plugin_name,
        api_compat_version=1,
        plugin_initializer=initialize_debputy_features,
        plugin_doc_path_resolver=lambda: _resolve_bundled_plugin_docs_path(
            plugin_name,
            initialize_debputy_features,
        ),
        plugin_loader=None,
        plugin_path="<loaded-via-test>",
    )
    feature_set = _empty_debputy_plugin_feature_set
    api = DebputyPluginInitializerProvider(
        debputy_plugin_metadata,
        feature_set,
        amd64_substitution,
    )
    api.load_plugin()

    plugin_names = getattr(request.function, "_required_plugins", [])
    if plugin_names:
        for plugin_name in plugin_names:
            plugin_metadata = find_json_plugin(
                [str(DEBPUTY_PLUGIN_ROOT_DIR)], plugin_name
            )
            print(
                f"debputy_plugin_feature_set - Loading extra plugin: {plugin_name} [{plugin_metadata.plugin_path}]"
            )
            plugin_under_test_provider = DebputyPluginInitializerProvider(
                plugin_metadata,
                feature_set,
                amd64_substitution,
            )
            plugin_under_test_provider.load_plugin()

    return feature_set


@pytest.fixture
def attribute_path(request) -> AttributePath:
    return AttributePath.builtin_path()[request.node.nodeid]


class ManifestParserFactory(typing.Protocol):

    def __call__(
        self,
        debian_dir: VirtualPath,
        *,
        dctrl_parser: DctrlParser | None = None,
    ) -> YAMLManifestParser: ...


@pytest.fixture()
def manifest_parser_amd64_factory(
    amd64_dpkg_architecture_variables,
    dpkg_arch_query,
    amd64_substitution,
    no_profiles_or_build_options,
    debputy_plugin_feature_set,
) -> ManifestParserFactory:

    def _factory(
        debian_dir: VirtualPath,
        *,
        dctrl_parser: DctrlParser | None = None,
    ) -> YAMLManifestParser:
        if dctrl_parser is None:
            dctrl_parser = DctrlParser(
                frozenset(),
                frozenset(),
                False,
                False,
                amd64_dpkg_architecture_variables,
                dpkg_arch_query,
                no_profiles_or_build_options,
            )

        dctrl = debian_dir.get("control")
        try:
            if dctrl is None:
                raise PureVirtualPathError("Missing d/control file")

            with dctrl.open() as fd:
                _, source_package, binary_packages = (
                    dctrl_parser.parse_source_debian_control(fd.readlines())
                )
        except PureVirtualPathError:
            raise ValueError(
                "The manifest_parser_amd64_factory requires `control` to be present and readable in the `debian_dir`"
            ) from None

        return YAMLManifestParser(
            "debian/test-debputy.manifest",
            source_package,
            binary_packages,
            amd64_substitution,
            # To align with the substitution, we do not allow this field to
            # be overwritten by the DctrlParser
            amd64_dpkg_architecture_variables,
            # Use the same instances
            assume_not_none(assume_not_none(dctrl_parser).dpkg_arch_query_table),
            assume_not_none(assume_not_none(dctrl_parser).deb_options_and_profiles),
            debputy_plugin_feature_set,
            "full",
            debian_dir=debian_dir,
        )

    return _factory
