import os

import pytest

from asdf import config_context
from asdf._asdf import AsdfFile, open_asdf
from asdf._entry_points import get_extensions
from asdf._tests._helpers import assert_tree_match
from asdf.exceptions import AsdfWarning
from asdf.extension import ExtensionProxy
from asdf.testing.helpers import yaml_to_asdf
from asdf.versioning import AsdfVersion


def test_no_warnings_get_extensions():
    """
    Smoke test for changes to the `importlib.metadata` entry points API.
    """

    get_extensions()


class TestExtension:
    __test__ = False

    def __init__(
        self,
        extension_uri=None,
        legacy_class_names=None,
        asdf_standard_requirement=None,
        types=None,
        tag_mapping=None,
        url_mapping=None,
    ):
        self._extension_uri = extension_uri
        self._legacy_class_names = set() if legacy_class_names is None else legacy_class_names
        self._asdf_standard_requirement = asdf_standard_requirement
        self._types = [] if types is None else types
        self._tag_mapping = [] if tag_mapping is None else tag_mapping
        self._url_mapping = [] if url_mapping is None else url_mapping

    @property
    def types(self):
        return self._types

    @property
    def tag_mapping(self):
        return self._tag_mapping

    @property
    def url_mapping(self):
        return self._url_mapping

    @property
    def extension_uri(self):
        return self._extension_uri

    @property
    def legacy_class_names(self):
        return self._legacy_class_names

    @property
    def asdf_standard_requirement(self):
        return self._asdf_standard_requirement


def test_asdf_file_version():
    with config_context() as config:
        config.default_version = "1.2.0"

        af = AsdfFile()
        assert af.version == AsdfVersion("1.2.0")
        assert af.version_string == "1.2.0"

        af = AsdfFile(version="1.3.0")
        assert af.version == AsdfVersion("1.3.0")
        assert af.version_string == "1.3.0"

        af = AsdfFile(version=AsdfVersion("1.3.0"))
        assert af.version == AsdfVersion("1.3.0")
        assert af.version_string == "1.3.0"

        with pytest.raises(ValueError, match=r"ASDF Standard version .* is not supported by asdf==.*"):
            AsdfFile(version="0.5.4")

        with pytest.raises(ValueError, match=r"ASDF Standard version .* is not supported by asdf==.*"):
            AsdfFile(version=AsdfVersion("0.5.4"))

        af = AsdfFile()

        af.version = "1.3.0"
        assert af.version == AsdfVersion("1.3.0")
        assert af.version_string == "1.3.0"

        af.version = AsdfVersion("1.4.0")
        assert af.version == AsdfVersion("1.4.0")
        assert af.version_string == "1.4.0"

        with pytest.raises(ValueError, match=r"ASDF Standard version .* is not supported by asdf==.*"):
            af.version = "0.5.4"

        with pytest.raises(ValueError, match=r"ASDF Standard version .* is not supported by asdf==.*"):
            af.version = AsdfVersion("2.5.4")


def test_asdf_file_extensions():
    af = AsdfFile()
    assert af.extensions == []

    extension = TestExtension(extension_uri="asdf://somewhere.org/extensions/foo-1.0")

    for arg in ([extension], extension):
        af = AsdfFile(extensions=arg)
        assert af.extensions == [ExtensionProxy(extension)]

        af = AsdfFile()
        af.extensions = arg
        assert af.extensions == [ExtensionProxy(extension)]

    msg = r"[The extensions parameter must be an extension.*, Extension must implement the Extension interface]"
    for arg in (object(), [object()]):
        with pytest.raises(TypeError, match=msg):
            AsdfFile(extensions=arg)


def test_asdf_file_version_requirement():
    extension_with_requirement = TestExtension(
        extension_uri="asdf://somewhere.org/extensions/foo-1.0",
        asdf_standard_requirement="==1.5.0",
    )

    # No warnings if the requirement is fulfilled:
    AsdfFile(version="1.5.0", extensions=[extension_with_requirement])

    # Version doesn't match the requirement, so we should see a warning
    # and the extension should not be enabled:
    with pytest.warns(AsdfWarning, match=r"does not support ASDF Standard 1.4.0"):
        af = AsdfFile(version="1.4.0", extensions=[extension_with_requirement])
        assert ExtensionProxy(extension_with_requirement) not in af.extensions

    # Version initially matches the requirement, but changing
    # the version on the AsdfFile invalidates it:
    af = AsdfFile(version="1.5.0", extensions=[extension_with_requirement])
    assert ExtensionProxy(extension_with_requirement) in af.extensions
    with pytest.warns(AsdfWarning, match=r"does not support ASDF Standard 1.4.0"):
        af.version = "1.4.0"
    assert ExtensionProxy(extension_with_requirement) not in af.extensions

    # Extension registered with the config should not provoke
    # a warning:
    with config_context() as config:
        config.add_extension(extension_with_requirement)
        af = AsdfFile(version="1.4.0")

        # ... unless the user explicitly requested the invalid extension:
        with pytest.warns(AsdfWarning, match=r"does not support ASDF Standard 1.4.0"):
            af = AsdfFile(version="1.4.0", extensions=[extension_with_requirement])


def test_open_asdf_extensions(tmp_path):
    extension = TestExtension(extension_uri="asdf://somewhere.org/extensions/foo-1.0")

    path = str(tmp_path / "test.asdf")

    with AsdfFile() as af:
        af.write_to(path)

    with open_asdf(path) as af:
        assert af.extensions == []

    for arg in ([extension], extension):
        with open_asdf(path, extensions=arg) as af:
            assert af.extensions == [ExtensionProxy(extension)]

    msg = r"[The extensions parameter must be an extension.*, Extension must implement the Extension interface]"
    for arg in (object(), [object()]):
        with pytest.raises(TypeError, match=msg), open_asdf(path, extensions=arg) as af:
            pass


def test_reading_extension_metadata():
    extension_with_uri = ExtensionProxy(
        TestExtension(extension_uri="asdf://somewhere.org/extensions/foo-1.0"),
        package_name="foo",
        package_version="1.2.3",
    )
    extension_without_uri = ExtensionProxy(
        TestExtension(),
        package_name="foo",
        package_version="1.2.3",
    )
    extension_with_legacy_class_names = ExtensionProxy(
        TestExtension(
            extension_uri="asdf://somewhere.org/extensions/with-legacy-1.0",
            legacy_class_names={"some.legacy.class.Name"},
        ),
        package_name="foo",
        package_version="1.2.3",
    )

    with config_context() as config:
        config.add_extension(extension_with_uri)
        config.add_extension(extension_without_uri)
        config.add_extension(extension_with_legacy_class_names)

        # Test missing history:
        content = "foo: bar"
        buff = yaml_to_asdf(content)
        open_asdf(buff)

        # Test the old history format:
        content = """
history:
  - !core/history_entry-1.0.0
    description: Once upon a time, there was a carnivorous panda.
  - !core/history_entry-1.0.0
    description: This entry intentionally left blank.
foo: bar
        """
        buff = yaml_to_asdf(content, version="1.0.0")
        open_asdf(buff)

        # Test matching by URI:
        content = """
history:
  extensions:
    - !core/extension_metadata-1.0.0
      extension_uri: asdf://somewhere.org/extensions/foo-1.0
      extension_class: some.unrecognized.extension.class.Name
        """
        buff = yaml_to_asdf(content)
        open_asdf(buff)

        # Test matching by legacy class name:
        content = """
history:
  extensions:
    - !core/extension_metadata-1.0.0
      extension_class: some.legacy.class.Name
        """
        buff = yaml_to_asdf(content)
        open_asdf(buff)

        # Warn when the URI is missing, even if there's
        # a class name match:
        content = f"""
history:
  extensions:
    - !core/extension_metadata-1.0.0
      extension_uri: some-missing-URI
      extension_class: {extension_with_uri.class_name}
        """
        buff = yaml_to_asdf(content)
        with pytest.warns(AsdfWarning, match=r"URI 'some-missing-URI'"):
            open_asdf(buff)

        # Warn when the class name is missing:
        content = """
history:
  extensions:
    - !core/extension_metadata-1.0.0
      extension_class: some.missing.class.Name
        """
        buff = yaml_to_asdf(content)
        with pytest.warns(AsdfWarning, match=r"class 'some.missing.class.Name'"):
            open_asdf(buff)

        # Warn when the package version is older:
        content = """
history:
  extensions:
    - !core/extension_metadata-1.0.0
      extension_uri: asdf://somewhere.org/extensions/foo-1.0
      extension_class: some.class.Name
      software: !core/software-1.0.0
        name: foo
        version: 9.2.4
        """
        buff = yaml_to_asdf(content)
        with pytest.warns(AsdfWarning, match=r"older package"):
            open_asdf(buff)

        # Shouldn't warn when the package version is later:
        content = """
history:
  extensions:
    - !core/extension_metadata-1.0.0
      extension_uri: asdf://somewhere.org/extensions/foo-1.0
      extension_class: some.class.Name
      software: !core/software-1.0.0
        name: foo
        version: 0.1.2
        """
        buff = yaml_to_asdf(content)
        open_asdf(buff)

        # Shouldn't receive a warning when the package
        # name changes, even if the version is later:
        content = """
history:
  extensions:
    - !core/extension_metadata-1.0.0
      extension_uri: asdf://somewhere.org/extensions/foo-1.0
      extension_class: some.class.Name
      software: !core/software-1.0.0
        name: bar
        version: 9.4.5
        """
        buff = yaml_to_asdf(content)
        open_asdf(buff)


def test_bad_input(tmp_path):
    """Make sure these functions behave properly with bad input"""
    text_file = str(tmp_path / "test.txt")

    with open(text_file, "w") as fh:
        fh.write("I <3 ASDF!!!!!")

    with pytest.raises(
        ValueError,
        match=r"Does not appear to be a ASDF file.",
    ):
        open_asdf(text_file)


def test_unclosed_file(tmp_path):
    """
    Issue #1006 reported an unclosed file when asdf.open fails
    This is a regression test for the fix in PR #1221
    """
    path = tmp_path / "empty.asdf"
    path.touch()

    with (
        pytest.raises(
            ValueError,
            match=r"Does not appear to be a ASDF file.",
        ),
        open_asdf(path),
    ):
        pass


def test_fsspec(tmp_path):
    """
    Issue #1146 reported errors when opening a fsspec 'file'
    This is a regression test for the fix in PR #1226
    """
    fsspec = pytest.importorskip("fsspec")

    tree = {"a": 1}
    af = AsdfFile(tree)
    fn = tmp_path / "test.asdf"
    af.write_to(fn)

    with fsspec.open(fn) as f:
        af = open_asdf(f)
        assert_tree_match(tree, af.tree)


def test_fsspec_http(httpserver):
    """
    Issue #1146 reported errors when opening a fsspec url (using the http
    filesystem)
    This is a regression test for the fix in PR #1228
    """
    fsspec = pytest.importorskip("fsspec")

    tree = {"a": 1}
    af = AsdfFile(tree)
    path = os.path.join(httpserver.tmpdir, "test")
    af.write_to(path)

    fn = httpserver.url + "test"
    with fsspec.open(fn) as f:
        af = open_asdf(f)
        assert_tree_match(tree, af.tree)
