# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE
# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt

from __future__ import annotations

import unittest

from astroid import builder, nodes, util
from astroid.exceptions import AttributeInferenceError


class NamedTupleTest(unittest.TestCase):
    def test_namedtuple_base(self) -> None:
        klass = builder.extract_node("""
        from collections import namedtuple

        class X(namedtuple("X", ["a", "b", "c"])):
           pass
        """)
        assert isinstance(klass, nodes.ClassDef)
        self.assertEqual(
            [anc.name for anc in klass.ancestors()], ["X", "tuple", "object"]
        )
        # See: https://github.com/pylint-dev/pylint/issues/5982
        self.assertNotIn("X", klass.locals)
        for anc in klass.ancestors():
            self.assertFalse(anc.parent is None)

    def test_namedtuple_inference(self) -> None:
        klass = builder.extract_node("""
        from collections import namedtuple

        name = "X"
        fields = ["a", "b", "c"]
        class X(namedtuple(name, fields)):
           pass
        """)
        assert isinstance(klass, nodes.ClassDef)
        base = next(base for base in klass.ancestors() if base.name == "X")
        self.assertSetEqual({"a", "b", "c"}, set(base.instance_attrs))

    def test_namedtuple_inference_failure(self) -> None:
        klass = builder.extract_node("""
        from collections import namedtuple

        def foo(fields):
           return __(namedtuple("foo", fields))
        """)
        self.assertIs(util.Uninferable, next(klass.infer()))

    def test_namedtuple_advanced_inference(self) -> None:
        # urlparse return an object of class ParseResult, which has a
        # namedtuple call and a mixin as base classes
        result = builder.extract_node("""
        from urllib.parse import urlparse

        result = __(urlparse('gopher://'))
        """)
        instance = next(result.infer())
        self.assertGreaterEqual(len(instance.getattr("scheme")), 1)
        self.assertGreaterEqual(len(instance.getattr("port")), 1)
        with self.assertRaises(AttributeInferenceError):
            instance.getattr("foo")
        self.assertGreaterEqual(len(instance.getattr("geturl")), 1)
        self.assertEqual(instance.name, "ParseResult")

    def test_namedtuple_instance_attrs(self) -> None:
        result = builder.extract_node("""
        from collections import namedtuple
        namedtuple('a', 'a b c')(1, 2, 3) #@
        """)
        inferred = next(result.infer())
        for name, attr in inferred.instance_attrs.items():
            self.assertEqual(attr[0].attrname, name)

    def test_namedtuple_uninferable_fields(self) -> None:
        node = builder.extract_node("""
        x = [A] * 2
        from collections import namedtuple
        l = namedtuple('a', x)
        l(1)
        """)
        inferred = next(node.infer())
        self.assertIs(util.Uninferable, inferred)

    def test_namedtuple_access_class_fields(self) -> None:
        node = builder.extract_node("""
        from collections import namedtuple
        Tuple = namedtuple("Tuple", "field other")
        Tuple #@
        """)
        inferred = next(node.infer())
        self.assertIn("field", inferred.locals)
        self.assertIn("other", inferred.locals)

    def test_namedtuple_rename_keywords(self) -> None:
        node = builder.extract_node("""
        from collections import namedtuple
        Tuple = namedtuple("Tuple", "abc def", rename=True)
        Tuple #@
        """)
        inferred = next(node.infer())
        self.assertIn("abc", inferred.locals)
        self.assertIn("_1", inferred.locals)

    def test_namedtuple_rename_duplicates(self) -> None:
        node = builder.extract_node("""
        from collections import namedtuple
        Tuple = namedtuple("Tuple", "abc abc abc", rename=True)
        Tuple #@
        """)
        inferred = next(node.infer())
        self.assertIn("abc", inferred.locals)
        self.assertIn("_1", inferred.locals)
        self.assertIn("_2", inferred.locals)

    def test_namedtuple_rename_uninferable(self) -> None:
        node = builder.extract_node("""
        from collections import namedtuple
        Tuple = namedtuple("Tuple", "a b c", rename=UNINFERABLE)
        Tuple #@
        """)
        inferred = next(node.infer())
        self.assertIn("a", inferred.locals)
        self.assertIn("b", inferred.locals)
        self.assertIn("c", inferred.locals)

    def test_namedtuple_func_form(self) -> None:
        node = builder.extract_node("""
        from collections import namedtuple
        Tuple = namedtuple(typename="Tuple", field_names="a b c", rename=UNINFERABLE)
        Tuple #@
        """)
        inferred = next(node.infer())
        self.assertEqual(inferred.name, "Tuple")
        self.assertIn("a", inferred.locals)
        self.assertIn("b", inferred.locals)
        self.assertIn("c", inferred.locals)

    def test_namedtuple_func_form_args_and_kwargs(self) -> None:
        node = builder.extract_node("""
        from collections import namedtuple
        Tuple = namedtuple("Tuple", field_names="a b c", rename=UNINFERABLE)
        Tuple #@
        """)
        inferred = next(node.infer())
        self.assertEqual(inferred.name, "Tuple")
        self.assertIn("a", inferred.locals)
        self.assertIn("b", inferred.locals)
        self.assertIn("c", inferred.locals)

    def test_namedtuple_bases_are_actually_names_not_nodes(self) -> None:
        node = builder.extract_node("""
        from collections import namedtuple
        Tuple = namedtuple("Tuple", field_names="a b c", rename=UNINFERABLE)
        Tuple #@
        """)
        inferred = next(node.infer())
        self.assertIsInstance(inferred, nodes.ClassDef)
        self.assertIsInstance(inferred.bases[0], nodes.Name)
        self.assertEqual(inferred.bases[0].name, "tuple")

    def test_invalid_label_does_not_crash_inference(self) -> None:
        code = """
        import collections
        a = collections.namedtuple( 'a', ['b c'] )
        a
        """
        node = builder.extract_node(code)
        inferred = next(node.infer())
        assert isinstance(inferred, nodes.ClassDef)
        assert "b" not in inferred.locals
        assert "c" not in inferred.locals

    def test_no_rename_duplicates_does_not_crash_inference(self) -> None:
        node = builder.extract_node("""
        from collections import namedtuple
        Tuple = namedtuple("Tuple", "abc abc")
        Tuple #@
        """)
        inferred = next(node.infer())
        self.assertIs(util.Uninferable, inferred)  # would raise ValueError

    def test_no_rename_keywords_does_not_crash_inference(self) -> None:
        node = builder.extract_node("""
        from collections import namedtuple
        Tuple = namedtuple("Tuple", "abc def")
        Tuple #@
        """)
        inferred = next(node.infer())
        self.assertIs(util.Uninferable, inferred)  # would raise ValueError

    def test_no_rename_nonident_does_not_crash_inference(self) -> None:
        node = builder.extract_node("""
        from collections import namedtuple
        Tuple = namedtuple("Tuple", "123 456")
        Tuple #@
        """)
        inferred = next(node.infer())
        self.assertIs(util.Uninferable, inferred)  # would raise ValueError

    def test_no_rename_underscore_does_not_crash_inference(self) -> None:
        node = builder.extract_node("""
        from collections import namedtuple
        Tuple = namedtuple("Tuple", "_1")
        Tuple #@
        """)
        inferred = next(node.infer())
        self.assertIs(util.Uninferable, inferred)  # would raise ValueError

    def test_invalid_typename_does_not_crash_inference(self) -> None:
        node = builder.extract_node("""
        from collections import namedtuple
        Tuple = namedtuple("123", "abc")
        Tuple #@
        """)
        inferred = next(node.infer())
        self.assertIs(util.Uninferable, inferred)  # would raise ValueError

    def test_keyword_typename_does_not_crash_inference(self) -> None:
        node = builder.extract_node("""
        from collections import namedtuple
        Tuple = namedtuple("while", "abc")
        Tuple #@
        """)
        inferred = next(node.infer())
        self.assertIs(util.Uninferable, inferred)  # would raise ValueError

    def test_typeerror_does_not_crash_inference(self) -> None:
        node = builder.extract_node("""
        from collections import namedtuple
        Tuple = namedtuple("Tuple", [123, 456])
        Tuple #@
        """)
        inferred = next(node.infer())
        # namedtuple converts all arguments to strings so these should be too
        # and catch on the isidentifier() check
        self.assertIs(util.Uninferable, inferred)

    def test_pathological_str_does_not_crash_inference(self) -> None:
        node = builder.extract_node("""
        from collections import namedtuple
        class Invalid:
            def __str__(self):
                return 123  # will raise TypeError
        Tuple = namedtuple("Tuple", [Invalid()])
        Tuple #@
        """)
        inferred = next(node.infer())
        self.assertIs(util.Uninferable, inferred)

    def test_name_as_typename(self) -> None:
        """Reported in https://github.com/pylint-dev/pylint/issues/7429 as a crash."""
        good_node, good_node_two, bad_node = builder.extract_node("""
            import collections
            collections.namedtuple(typename="MyTuple", field_names=["birth_date", "city"])  #@
            collections.namedtuple("MyTuple", field_names=["birth_date", "city"])  #@
            collections.namedtuple(["birth_date", "city"], typename="MyTuple")  #@
        """)
        good_inferred = next(good_node.infer())
        assert isinstance(good_inferred, nodes.ClassDef)
        good_node_two_inferred = next(good_node_two.infer())
        assert isinstance(good_node_two_inferred, nodes.ClassDef)
        bad_node_inferred = next(bad_node.infer())
        assert bad_node_inferred == util.Uninferable
