# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license

import unittest

import dns.immutable
import dns._immutable_ctx


class ImmutableTestCase(unittest.TestCase):
    def test_immutable_dict_hash(self):
        d1 = dns.immutable.Dict({"a": 1, "b": 2})
        d2 = dns.immutable.Dict({"b": 2, "a": 1})
        d3 = {"b": 2, "a": 1}
        self.assertEqual(d1, d2)
        self.assertEqual(d2, d3)
        self.assertEqual(hash(d1), hash(d2))

    def test_immutable_dict_hash_cache(self):
        d = dns.immutable.Dict({"a": 1, "b": 2})
        self.assertEqual(d._hash, None)
        h1 = hash(d)
        self.assertEqual(d._hash, h1)
        h2 = hash(d)
        self.assertEqual(h1, h2)

    def test_constify(self):
        items = (
            (bytearray([1, 2, 3]), b"\x01\x02\x03"),
            ((1, 2, 3), (1, 2, 3)),
            ((1, [2], 3), (1, (2,), 3)),
            ([1, 2, 3], (1, 2, 3)),
            ([1, {"a": [1, 2]}], (1, dns.immutable.Dict({"a": (1, 2)}))),
            ("hi", "hi"),
            (b"hi", b"hi"),
        )
        for input, expected in items:
            self.assertEqual(dns.immutable.constify(input), expected)
        self.assertIsInstance(dns.immutable.constify({"a": 1}), dns.immutable.Dict)


class DecoratorTestCase(unittest.TestCase):
    immutable_module = dns._immutable_ctx

    def make_classes(self):
        class A:
            def __init__(self, a, akw=10):
                self.a = a
                self.akw = akw

        class B(A):
            def __init__(self, a, b):
                super().__init__(a, akw=20)
                self.b = b

        B = self.immutable_module.immutable(B)

        # note C is immutable by inheritance
        class C(B):
            def __init__(self, a, b, c):
                super().__init__(a, b)
                self.c = c

        C = self.immutable_module.immutable(C)

        class SA:
            __slots__ = ("a", "akw")

            def __init__(self, a, akw=10):
                self.a = a
                self.akw = akw

        class SB(A):
            __slots__ = "b"

            def __init__(self, a, b):
                super().__init__(a, akw=20)
                self.b = b

        SB = self.immutable_module.immutable(SB)

        # note SC is immutable by inheritance and has no slots of its own
        class SC(SB):
            def __init__(self, a, b, c):
                super().__init__(a, b)
                self.c = c

        SC = self.immutable_module.immutable(SC)

        return ((A, B, C), (SA, SB, SC))

    def test_basic(self):
        for A, B, C in self.make_classes():
            a = A(1)
            self.assertEqual(a.a, 1)
            self.assertEqual(a.akw, 10)
            b = B(11, 21)
            self.assertEqual(b.a, 11)
            self.assertEqual(b.akw, 20)
            self.assertEqual(b.b, 21)
            c = C(111, 211, 311)
            self.assertEqual(c.a, 111)
            self.assertEqual(c.akw, 20)
            self.assertEqual(c.b, 211)
            self.assertEqual(c.c, 311)
            # changing A is ok!
            a.a = 11
            self.assertEqual(a.a, 11)
            # changing B is not!
            with self.assertRaises(TypeError):
                b.a = 11
            with self.assertRaises(TypeError):
                del b.a

    def test_constructor_deletes_attribute(self):
        class A:
            def __init__(self, a):
                self.a = a
                self.b = a
                del self.b

        A = self.immutable_module.immutable(A)
        a = A(10)
        self.assertEqual(a.a, 10)
        self.assertFalse(hasattr(a, "b"))

    def test_no_collateral_damage(self):
        # A and B are immutable but not related.  The magic that lets
        # us write to immutable things while initializing B should not let
        # B mess with A.

        class A:
            def __init__(self, a):
                self.a = a

        A = self.immutable_module.immutable(A)

        class B:
            def __init__(self, a, b):
                self.b = a.a + b
                # rudely attempt to mutate innocent immutable bystander 'a'
                a.a = 1000

        B = self.immutable_module.immutable(B)

        a = A(10)
        self.assertEqual(a.a, 10)
        with self.assertRaises(TypeError):
            B(a, 20)
        self.assertEqual(a.a, 10)
