import warnings
from enum import Enum

from ..hdl.ast import *
from .utils import *


class UnsignedEnum(Enum):
    FOO = 1
    BAR = 2
    BAZ = 3


class SignedEnum(Enum):
    FOO = -1
    BAR =  0
    BAZ = +1


class StringEnum(Enum):
    FOO = "a"
    BAR = "b"


class ShapeTestCase(FHDLTestCase):
    def test_make(self):
        s1 = Shape()
        self.assertEqual(s1.width, 1)
        self.assertEqual(s1.signed, False)
        s2 = Shape(signed=True)
        self.assertEqual(s2.width, 1)
        self.assertEqual(s2.signed, True)
        s3 = Shape(3, True)
        self.assertEqual(s3.width, 3)
        self.assertEqual(s3.signed, True)

    def test_make_wrong(self):
        with self.assertRaises(TypeError,
                msg="Width must be a non-negative integer, not -1"):
            Shape(-1)

    def test_tuple(self):
        width, signed = Shape()
        self.assertEqual(width, 1)
        self.assertEqual(signed, False)

    def test_unsigned(self):
        s1 = unsigned(2)
        self.assertIsInstance(s1, Shape)
        self.assertEqual(s1.width, 2)
        self.assertEqual(s1.signed, False)

    def test_signed(self):
        s1 = signed(2)
        self.assertIsInstance(s1, Shape)
        self.assertEqual(s1.width, 2)
        self.assertEqual(s1.signed, True)

    def test_cast_shape(self):
        s1 = Shape.cast(unsigned(1))
        self.assertEqual(s1.width, 1)
        self.assertEqual(s1.signed, False)
        s2 = Shape.cast(signed(3))
        self.assertEqual(s2.width, 3)
        self.assertEqual(s2.signed, True)

    def test_cast_int(self):
        s1 = Shape.cast(2)
        self.assertEqual(s1.width, 2)
        self.assertEqual(s1.signed, False)

    def test_cast_int_wrong(self):
        with self.assertRaises(TypeError,
                msg="Width must be a non-negative integer, not -1"):
            Shape.cast(-1)

    def test_cast_tuple(self):
        with warnings.catch_warnings():
            warnings.filterwarnings(action="ignore", category=DeprecationWarning)
            s1 = Shape.cast((1, True))
            self.assertEqual(s1.width, 1)
            self.assertEqual(s1.signed, True)

    def test_cast_tuple_wrong(self):
        with warnings.catch_warnings():
            warnings.filterwarnings(action="ignore", category=DeprecationWarning)
            with self.assertRaises(TypeError,
                    msg="Width must be a non-negative integer, not -1"):
                Shape.cast((-1, True))

    def test_cast_range(self):
        s1 = Shape.cast(range(0, 8))
        self.assertEqual(s1.width, 3)
        self.assertEqual(s1.signed, False)
        s2 = Shape.cast(range(0, 9))
        self.assertEqual(s2.width, 4)
        self.assertEqual(s2.signed, False)
        s3 = Shape.cast(range(-7, 8))
        self.assertEqual(s3.width, 4)
        self.assertEqual(s3.signed, True)
        s4 = Shape.cast(range(0, 1))
        self.assertEqual(s4.width, 1)
        self.assertEqual(s4.signed, False)
        s5 = Shape.cast(range(-1, 0))
        self.assertEqual(s5.width, 1)
        self.assertEqual(s5.signed, True)
        s6 = Shape.cast(range(0, 0))
        self.assertEqual(s6.width, 0)
        self.assertEqual(s6.signed, False)
        s7 = Shape.cast(range(-1, -1))
        self.assertEqual(s7.width, 0)
        self.assertEqual(s7.signed, True)

    def test_cast_enum(self):
        s1 = Shape.cast(UnsignedEnum)
        self.assertEqual(s1.width, 2)
        self.assertEqual(s1.signed, False)
        s2 = Shape.cast(SignedEnum)
        self.assertEqual(s2.width, 2)
        self.assertEqual(s2.signed, True)

    def test_cast_enum_bad(self):
        with self.assertRaises(TypeError,
                msg="Only enumerations with integer values can be used as value shapes"):
            Shape.cast(StringEnum)

    def test_cast_bad(self):
        with self.assertRaises(TypeError,
                msg="Object 'foo' cannot be used as value shape"):
            Shape.cast("foo")


class ValueTestCase(FHDLTestCase):
    def test_cast(self):
        self.assertIsInstance(Value.cast(0), Const)
        self.assertIsInstance(Value.cast(True), Const)
        c = Const(0)
        self.assertIs(Value.cast(c), c)
        with self.assertRaises(TypeError,
                msg="Object 'str' cannot be converted to an nMigen value"):
            Value.cast("str")

    def test_cast_enum(self):
        e1 = Value.cast(UnsignedEnum.FOO)
        self.assertIsInstance(e1, Const)
        self.assertEqual(e1.shape(), unsigned(2))
        e2 = Value.cast(SignedEnum.FOO)
        self.assertIsInstance(e2, Const)
        self.assertEqual(e2.shape(), signed(2))

    def test_cast_enum_wrong(self):
        with self.assertRaises(TypeError,
                msg="Only enumerations with integer values can be used as value shapes"):
            Value.cast(StringEnum.FOO)

    def test_bool(self):
        with self.assertRaises(TypeError,
                msg="Attempted to convert nMigen value to boolean"):
            if Const(0):
                pass

    def test_len(self):
        self.assertEqual(len(Const(10)), 4)

    def test_getitem_int(self):
        s1 = Const(10)[0]
        self.assertIsInstance(s1, Slice)
        self.assertEqual(s1.start, 0)
        self.assertEqual(s1.stop, 1)
        s2 = Const(10)[-1]
        self.assertIsInstance(s2, Slice)
        self.assertEqual(s2.start, 3)
        self.assertEqual(s2.stop, 4)
        with self.assertRaises(IndexError,
                msg="Cannot index 5 bits into 4-bit value"):
            Const(10)[5]

    def test_getitem_slice(self):
        s1 = Const(10)[1:3]
        self.assertIsInstance(s1, Slice)
        self.assertEqual(s1.start, 1)
        self.assertEqual(s1.stop, 3)
        s2 = Const(10)[1:-2]
        self.assertIsInstance(s2, Slice)
        self.assertEqual(s2.start, 1)
        self.assertEqual(s2.stop, 2)
        s3 = Const(31)[::2]
        self.assertIsInstance(s3, Cat)
        self.assertIsInstance(s3.parts[0], Slice)
        self.assertEqual(s3.parts[0].start, 0)
        self.assertEqual(s3.parts[0].stop, 1)
        self.assertIsInstance(s3.parts[1], Slice)
        self.assertEqual(s3.parts[1].start, 2)
        self.assertEqual(s3.parts[1].stop, 3)
        self.assertIsInstance(s3.parts[2], Slice)
        self.assertEqual(s3.parts[2].start, 4)
        self.assertEqual(s3.parts[2].stop, 5)

    def test_getitem_wrong(self):
        with self.assertRaises(TypeError,
                msg="Cannot index value with 'str'"):
            Const(31)["str"]


class ConstTestCase(FHDLTestCase):
    def test_shape(self):
        self.assertEqual(Const(0).shape(),   unsigned(1))
        self.assertIsInstance(Const(0).shape(), Shape)
        self.assertEqual(Const(1).shape(),   unsigned(1))
        self.assertEqual(Const(10).shape(),  unsigned(4))
        self.assertEqual(Const(-10).shape(), signed(5))

        self.assertEqual(Const(1, 4).shape(),          unsigned(4))
        self.assertEqual(Const(-1, 4).shape(),         signed(4))
        self.assertEqual(Const(1, signed(4)).shape(),  signed(4))
        self.assertEqual(Const(0, unsigned(0)).shape(), unsigned(0))

    def test_shape_wrong(self):
        with self.assertRaises(TypeError,
                msg="Width must be a non-negative integer, not -1"):
            Const(1, -1)

    def test_normalization(self):
        self.assertEqual(Const(0b10110, signed(5)).value, -10)

    def test_value(self):
        self.assertEqual(Const(10).value, 10)

    def test_repr(self):
        self.assertEqual(repr(Const(10)),  "(const 4'd10)")
        self.assertEqual(repr(Const(-10)), "(const 5'sd-10)")

    def test_hash(self):
        with self.assertRaises(TypeError):
            hash(Const(0))


class OperatorTestCase(FHDLTestCase):
    def test_bool(self):
        v = Const(0, 4).bool()
        self.assertEqual(repr(v), "(b (const 4'd0))")
        self.assertEqual(v.shape(), unsigned(1))

    def test_invert(self):
        v = ~Const(0, 4)
        self.assertEqual(repr(v), "(~ (const 4'd0))")
        self.assertEqual(v.shape(), unsigned(4))

    def test_as_unsigned(self):
        v = Const(-1, signed(4)).as_unsigned()
        self.assertEqual(repr(v), "(u (const 4'sd-1))")
        self.assertEqual(v.shape(), unsigned(4))

    def test_as_signed(self):
        v = Const(1, unsigned(4)).as_signed()
        self.assertEqual(repr(v), "(s (const 4'd1))")
        self.assertEqual(v.shape(), signed(4))

    def test_neg(self):
        v1 = -Const(0, unsigned(4))
        self.assertEqual(repr(v1), "(- (const 4'd0))")
        self.assertEqual(v1.shape(), signed(5))
        v2 = -Const(0, signed(4))
        self.assertEqual(repr(v2), "(- (const 4'sd0))")
        self.assertEqual(v2.shape(), signed(5))

    def test_add(self):
        v1 = Const(0, unsigned(4)) + Const(0, unsigned(6))
        self.assertEqual(repr(v1), "(+ (const 4'd0) (const 6'd0))")
        self.assertEqual(v1.shape(), unsigned(7))
        v2 = Const(0, signed(4)) + Const(0, signed(6))
        self.assertEqual(v2.shape(), signed(7))
        v3 = Const(0, signed(4)) + Const(0, unsigned(4))
        self.assertEqual(v3.shape(), signed(6))
        v4 = Const(0, unsigned(4)) + Const(0, signed(4))
        self.assertEqual(v4.shape(), signed(6))
        v5 = 10 + Const(0, 4)
        self.assertEqual(v5.shape(), unsigned(5))

    def test_sub(self):
        v1 = Const(0, unsigned(4)) - Const(0, unsigned(6))
        self.assertEqual(repr(v1), "(- (const 4'd0) (const 6'd0))")
        self.assertEqual(v1.shape(), unsigned(7))
        v2 = Const(0, signed(4)) - Const(0, signed(6))
        self.assertEqual(v2.shape(), signed(7))
        v3 = Const(0, signed(4)) - Const(0, unsigned(4))
        self.assertEqual(v3.shape(), signed(6))
        v4 = Const(0, unsigned(4)) - Const(0, signed(4))
        self.assertEqual(v4.shape(), signed(6))
        v5 = 10 - Const(0, 4)
        self.assertEqual(v5.shape(), unsigned(5))

    def test_mul(self):
        v1 = Const(0, unsigned(4)) * Const(0, unsigned(6))
        self.assertEqual(repr(v1), "(* (const 4'd0) (const 6'd0))")
        self.assertEqual(v1.shape(), unsigned(10))
        v2 = Const(0, signed(4)) * Const(0, signed(6))
        self.assertEqual(v2.shape(), signed(10))
        v3 = Const(0, signed(4)) * Const(0, unsigned(4))
        self.assertEqual(v3.shape(), signed(8))
        v5 = 10 * Const(0, 4)
        self.assertEqual(v5.shape(), unsigned(8))

    def test_mod(self):
        v1 = Const(0, unsigned(4)) % Const(0, unsigned(6))
        self.assertEqual(repr(v1), "(% (const 4'd0) (const 6'd0))")
        self.assertEqual(v1.shape(), unsigned(4))
        v3 = Const(0, signed(4)) % Const(0, unsigned(4))
        self.assertEqual(v3.shape(), signed(4))
        v5 = 10 % Const(0, 4)
        self.assertEqual(v5.shape(), unsigned(4))

    def test_mod_wrong(self):
        with self.assertRaises(NotImplementedError,
                msg="Division by a signed value is not supported"):
            Const(0, signed(4)) % Const(0, signed(6))

    def test_floordiv(self):
        v1 = Const(0, unsigned(4)) // Const(0, unsigned(6))
        self.assertEqual(repr(v1), "(// (const 4'd0) (const 6'd0))")
        self.assertEqual(v1.shape(), unsigned(4))
        v3 = Const(0, signed(4)) // Const(0, unsigned(4))
        self.assertEqual(v3.shape(), signed(4))
        v5 = 10 // Const(0, 4)
        self.assertEqual(v5.shape(), unsigned(4))

    def test_floordiv_wrong(self):
        with self.assertRaises(NotImplementedError,
                msg="Division by a signed value is not supported"):
            Const(0, signed(4)) // Const(0, signed(6))

    def test_and(self):
        v1 = Const(0, unsigned(4)) & Const(0, unsigned(6))
        self.assertEqual(repr(v1), "(& (const 4'd0) (const 6'd0))")
        self.assertEqual(v1.shape(), unsigned(6))
        v2 = Const(0, signed(4)) & Const(0, signed(6))
        self.assertEqual(v2.shape(), signed(6))
        v3 = Const(0, signed(4)) & Const(0, unsigned(4))
        self.assertEqual(v3.shape(), signed(5))
        v4 = Const(0, unsigned(4)) & Const(0, signed(4))
        self.assertEqual(v4.shape(), signed(5))
        v5 = 10 & Const(0, 4)
        self.assertEqual(v5.shape(), unsigned(4))

    def test_or(self):
        v1 = Const(0, unsigned(4)) | Const(0, unsigned(6))
        self.assertEqual(repr(v1), "(| (const 4'd0) (const 6'd0))")
        self.assertEqual(v1.shape(), unsigned(6))
        v2 = Const(0, signed(4)) | Const(0, signed(6))
        self.assertEqual(v2.shape(), signed(6))
        v3 = Const(0, signed(4)) | Const(0, unsigned(4))
        self.assertEqual(v3.shape(), signed(5))
        v4 = Const(0, unsigned(4)) | Const(0, signed(4))
        self.assertEqual(v4.shape(), signed(5))
        v5 = 10 | Const(0, 4)
        self.assertEqual(v5.shape(), unsigned(4))

    def test_xor(self):
        v1 = Const(0, unsigned(4)) ^ Const(0, unsigned(6))
        self.assertEqual(repr(v1), "(^ (const 4'd0) (const 6'd0))")
        self.assertEqual(v1.shape(), unsigned(6))
        v2 = Const(0, signed(4)) ^ Const(0, signed(6))
        self.assertEqual(v2.shape(), signed(6))
        v3 = Const(0, signed(4)) ^ Const(0, unsigned(4))
        self.assertEqual(v3.shape(), signed(5))
        v4 = Const(0, unsigned(4)) ^ Const(0, signed(4))
        self.assertEqual(v4.shape(), signed(5))
        v5 = 10 ^ Const(0, 4)
        self.assertEqual(v5.shape(), unsigned(4))

    def test_shl(self):
        v1 = Const(1, 4) << Const(4)
        self.assertEqual(repr(v1), "(<< (const 4'd1) (const 3'd4))")
        self.assertEqual(v1.shape(), unsigned(11))

    def test_shl_wrong(self):
        with self.assertRaises(NotImplementedError,
                msg="Shift by a signed value is not supported"):
            1 << Const(0, signed(6))
        with self.assertRaises(NotImplementedError,
                msg="Shift by a signed value is not supported"):
            Const(1, unsigned(4)) << -1

    def test_shr(self):
        v1 = Const(1, 4) >> Const(4)
        self.assertEqual(repr(v1), "(>> (const 4'd1) (const 3'd4))")
        self.assertEqual(v1.shape(), unsigned(4))

    def test_shr_wrong(self):
        with self.assertRaises(NotImplementedError,
                msg="Shift by a signed value is not supported"):
            1 << Const(0, signed(6))
        with self.assertRaises(NotImplementedError,
                msg="Shift by a signed value is not supported"):
            Const(1, unsigned(4)) << -1

    def test_lt(self):
        v = Const(0, 4) < Const(0, 6)
        self.assertEqual(repr(v), "(< (const 4'd0) (const 6'd0))")
        self.assertEqual(v.shape(), unsigned(1))

    def test_le(self):
        v = Const(0, 4) <= Const(0, 6)
        self.assertEqual(repr(v), "(<= (const 4'd0) (const 6'd0))")
        self.assertEqual(v.shape(), unsigned(1))

    def test_gt(self):
        v = Const(0, 4) > Const(0, 6)
        self.assertEqual(repr(v), "(> (const 4'd0) (const 6'd0))")
        self.assertEqual(v.shape(), unsigned(1))

    def test_ge(self):
        v = Const(0, 4) >= Const(0, 6)
        self.assertEqual(repr(v), "(>= (const 4'd0) (const 6'd0))")
        self.assertEqual(v.shape(), unsigned(1))

    def test_eq(self):
        v = Const(0, 4) == Const(0, 6)
        self.assertEqual(repr(v), "(== (const 4'd0) (const 6'd0))")
        self.assertEqual(v.shape(), unsigned(1))

    def test_ne(self):
        v = Const(0, 4) != Const(0, 6)
        self.assertEqual(repr(v), "(!= (const 4'd0) (const 6'd0))")
        self.assertEqual(v.shape(), unsigned(1))

    def test_mux(self):
        s  = Const(0)
        v1 = Mux(s, Const(0, unsigned(4)), Const(0, unsigned(6)))
        self.assertEqual(repr(v1), "(m (const 1'd0) (const 4'd0) (const 6'd0))")
        self.assertEqual(v1.shape(), unsigned(6))
        v2 = Mux(s, Const(0, signed(4)), Const(0, signed(6)))
        self.assertEqual(v2.shape(), signed(6))
        v3 = Mux(s, Const(0, signed(4)), Const(0, unsigned(4)))
        self.assertEqual(v3.shape(), signed(5))
        v4 = Mux(s, Const(0, unsigned(4)), Const(0, signed(4)))
        self.assertEqual(v4.shape(), signed(5))

    def test_mux_wide(self):
        s = Const(0b100)
        v = Mux(s, Const(0, unsigned(4)), Const(0, unsigned(6)))
        self.assertEqual(repr(v), "(m (b (const 3'd4)) (const 4'd0) (const 6'd0))")

    def test_mux_bool(self):
        v = Mux(True, Const(0), Const(0))
        self.assertEqual(repr(v), "(m (const 1'd1) (const 1'd0) (const 1'd0))")

    def test_bool(self):
        v = Const(0).bool()
        self.assertEqual(repr(v), "(b (const 1'd0))")
        self.assertEqual(v.shape(), unsigned(1))

    def test_any(self):
        v = Const(0b101).any()
        self.assertEqual(repr(v), "(r| (const 3'd5))")

    def test_all(self):
        v = Const(0b101).all()
        self.assertEqual(repr(v), "(r& (const 3'd5))")

    def test_xor(self):
        v = Const(0b101).xor()
        self.assertEqual(repr(v), "(r^ (const 3'd5))")

    def test_matches(self):
        s = Signal(4)
        self.assertRepr(s.matches(), "(const 1'd0)")
        self.assertRepr(s.matches(1), """
        (== (sig s) (const 1'd1))
        """)
        self.assertRepr(s.matches(0, 1), """
        (r| (cat (== (sig s) (const 1'd0)) (== (sig s) (const 1'd1))))
        """)
        self.assertRepr(s.matches("10--"), """
        (== (& (sig s) (const 4'd12)) (const 4'd8))
        """)
        self.assertRepr(s.matches("1 0--"), """
        (== (& (sig s) (const 4'd12)) (const 4'd8))
        """)

    def test_matches_enum(self):
        s = Signal(SignedEnum)
        self.assertRepr(s.matches(SignedEnum.FOO), """
        (== (sig s) (const 1'sd-1))
        """)

    def test_matches_width_wrong(self):
        s = Signal(4)
        with self.assertRaises(SyntaxError,
                msg="Match pattern '--' must have the same width as match value (which is 4)"):
            s.matches("--")
        with self.assertWarns(SyntaxWarning,
                msg="Match pattern '10110' is wider than match value (which has width 4); "
                    "comparison will never be true"):
            s.matches(0b10110)

    def test_matches_bits_wrong(self):
        s = Signal(4)
        with self.assertRaises(SyntaxError,
                msg="Match pattern 'abc' must consist of 0, 1, and - (don't care) bits, "
                    "and may include whitespace"):
            s.matches("abc")

    def test_matches_pattern_wrong(self):
        s = Signal(4)
        with self.assertRaises(SyntaxError,
                msg="Match pattern must be an integer, a string, or an enumeration, not 1.0"):
            s.matches(1.0)

    def test_hash(self):
        with self.assertRaises(TypeError):
            hash(Const(0) + Const(0))


class SliceTestCase(FHDLTestCase):
    def test_shape(self):
        s1 = Const(10)[2]
        self.assertEqual(s1.shape(), unsigned(1))
        self.assertIsInstance(s1.shape(), Shape)
        s2 = Const(-10)[0:2]
        self.assertEqual(s2.shape(), unsigned(2))

    def test_start_end_negative(self):
        c  = Const(0, 8)
        s1 = Slice(c, 0, -1)
        self.assertEqual((s1.start, s1.stop), (0, 7))
        s1 = Slice(c, -4, -1)
        self.assertEqual((s1.start, s1.stop), (4, 7))

    def test_start_end_wrong(self):
        with self.assertRaises(TypeError,
                msg="Slice start must be an integer, not 'x'"):
            Slice(0, "x", 1)
        with self.assertRaises(TypeError,
                msg="Slice stop must be an integer, not 'x'"):
            Slice(0, 1, "x")

    def test_start_end_out_of_range(self):
        c = Const(0, 8)
        with self.assertRaises(IndexError,
                msg="Cannot start slice 10 bits into 8-bit value"):
            Slice(c, 10, 12)
        with self.assertRaises(IndexError,
                msg="Cannot stop slice 12 bits into 8-bit value"):
            Slice(c, 0, 12)
        with self.assertRaises(IndexError,
                msg="Slice start 4 must be less than slice stop 2"):
            Slice(c, 4, 2)

    def test_repr(self):
        s1 = Const(10)[2]
        self.assertEqual(repr(s1), "(slice (const 4'd10) 2:3)")


class BitSelectTestCase(FHDLTestCase):
    def setUp(self):
        self.c = Const(0, 8)
        self.s = Signal(range(self.c.width))

    def test_shape(self):
        s1 = self.c.bit_select(self.s, 2)
        self.assertIsInstance(s1, Part)
        self.assertEqual(s1.shape(), unsigned(2))
        self.assertIsInstance(s1.shape(), Shape)
        s2 = self.c.bit_select(self.s, 0)
        self.assertIsInstance(s2, Part)
        self.assertEqual(s2.shape(), unsigned(0))

    def test_stride(self):
        s1 = self.c.bit_select(self.s, 2)
        self.assertIsInstance(s1, Part)
        self.assertEqual(s1.stride, 1)

    def test_const(self):
        s1 = self.c.bit_select(1, 2)
        self.assertIsInstance(s1, Slice)
        self.assertRepr(s1, """(slice (const 8'd0) 1:3)""")

    def test_width_wrong(self):
        with self.assertRaises(TypeError):
            self.c.bit_select(self.s, -1)

    def test_repr(self):
        s = self.c.bit_select(self.s, 2)
        self.assertEqual(repr(s), "(part (const 8'd0) (sig s) 2 1)")


class WordSelectTestCase(FHDLTestCase):
    def setUp(self):
        self.c = Const(0, 8)
        self.s = Signal(range(self.c.width))

    def test_shape(self):
        s1 = self.c.word_select(self.s, 2)
        self.assertIsInstance(s1, Part)
        self.assertEqual(s1.shape(), unsigned(2))
        self.assertIsInstance(s1.shape(), Shape)

    def test_stride(self):
        s1 = self.c.word_select(self.s, 2)
        self.assertIsInstance(s1, Part)
        self.assertEqual(s1.stride, 2)

    def test_const(self):
        s1 = self.c.word_select(1, 2)
        self.assertIsInstance(s1, Slice)
        self.assertRepr(s1, """(slice (const 8'd0) 2:4)""")

    def test_width_wrong(self):
        with self.assertRaises(TypeError):
            self.c.word_select(self.s, 0)
        with self.assertRaises(TypeError):
            self.c.word_select(self.s, -1)

    def test_repr(self):
        s = self.c.word_select(self.s, 2)
        self.assertEqual(repr(s), "(part (const 8'd0) (sig s) 2 2)")


class CatTestCase(FHDLTestCase):
    def test_shape(self):
        c0 = Cat()
        self.assertEqual(c0.shape(), unsigned(0))
        self.assertIsInstance(c0.shape(), Shape)
        c1 = Cat(Const(10))
        self.assertEqual(c1.shape(), unsigned(4))
        c2 = Cat(Const(10), Const(1))
        self.assertEqual(c2.shape(), unsigned(5))
        c3 = Cat(Const(10), Const(1), Const(0))
        self.assertEqual(c3.shape(), unsigned(6))

    def test_repr(self):
        c1 = Cat(Const(10), Const(1))
        self.assertEqual(repr(c1), "(cat (const 4'd10) (const 1'd1))")


class ReplTestCase(FHDLTestCase):
    def test_shape(self):
        s1 = Repl(Const(10), 3)
        self.assertEqual(s1.shape(), unsigned(12))
        self.assertIsInstance(s1.shape(), Shape)
        s2 = Repl(Const(10), 0)
        self.assertEqual(s2.shape(), unsigned(0))

    def test_count_wrong(self):
        with self.assertRaises(TypeError):
            Repl(Const(10), -1)
        with self.assertRaises(TypeError):
            Repl(Const(10), "str")

    def test_repr(self):
        s = Repl(Const(10), 3)
        self.assertEqual(repr(s), "(repl (const 4'd10) 3)")


class ArrayTestCase(FHDLTestCase):
    def test_acts_like_array(self):
        a = Array([1,2,3])
        self.assertSequenceEqual(a, [1,2,3])
        self.assertEqual(a[1], 2)
        a[1] = 4
        self.assertSequenceEqual(a, [1,4,3])
        del a[1]
        self.assertSequenceEqual(a, [1,3])
        a.insert(1, 2)
        self.assertSequenceEqual(a, [1,2,3])

    def test_becomes_immutable(self):
        a = Array([1,2,3])
        s1 = Signal(range(len(a)))
        s2 = Signal(range(len(a)))
        v1 = a[s1]
        v2 = a[s2]
        with self.assertRaisesRegex(ValueError,
                regex=r"^Array can no longer be mutated after it was indexed with a value at "):
            a[1] = 2
        with self.assertRaisesRegex(ValueError,
                regex=r"^Array can no longer be mutated after it was indexed with a value at "):
            del a[1]
        with self.assertRaisesRegex(ValueError,
                regex=r"^Array can no longer be mutated after it was indexed with a value at "):
            a.insert(1, 2)

    def test_repr(self):
        a = Array([1,2,3])
        self.assertEqual(repr(a), "(array mutable [1, 2, 3])")
        s = Signal(range(len(a)))
        v = a[s]
        self.assertEqual(repr(a), "(array [1, 2, 3])")


class ArrayProxyTestCase(FHDLTestCase):
    def test_index_shape(self):
        m = Array(Array(x * y for y in range(1, 4)) for x in range(1, 4))
        a = Signal(range(3))
        b = Signal(range(3))
        v = m[a][b]
        self.assertEqual(v.shape(), unsigned(4))

    def test_attr_shape(self):
        from collections import namedtuple
        pair = namedtuple("pair", ("p", "n"))
        a = Array(pair(i, -i) for i in range(10))
        s = Signal(range(len(a)))
        v = a[s]
        self.assertEqual(v.p.shape(), unsigned(4))
        self.assertEqual(v.n.shape(), signed(6))

    def test_repr(self):
        a = Array([1, 2, 3])
        s = Signal(range(3))
        v = a[s]
        self.assertEqual(repr(v), "(proxy (array [1, 2, 3]) (sig s))")


class SignalTestCase(FHDLTestCase):
    def test_shape(self):
        s1 = Signal()
        self.assertEqual(s1.shape(), unsigned(1))
        self.assertIsInstance(s1.shape(), Shape)
        s2 = Signal(2)
        self.assertEqual(s2.shape(), unsigned(2))
        s3 = Signal(unsigned(2))
        self.assertEqual(s3.shape(), unsigned(2))
        s4 = Signal(signed(2))
        self.assertEqual(s4.shape(), signed(2))
        s5 = Signal(0)
        self.assertEqual(s5.shape(), unsigned(0))
        s6 = Signal(range(16))
        self.assertEqual(s6.shape(), unsigned(4))
        s7 = Signal(range(4, 16))
        self.assertEqual(s7.shape(), unsigned(4))
        s8 = Signal(range(-4, 16))
        self.assertEqual(s8.shape(), signed(5))
        s9 = Signal(range(-20, 16))
        self.assertEqual(s9.shape(), signed(6))
        s10 = Signal(range(0))
        self.assertEqual(s10.shape(), unsigned(0))
        s11 = Signal(range(1))
        self.assertEqual(s11.shape(), unsigned(1))

    def test_shape_wrong(self):
        with self.assertRaises(TypeError,
                msg="Width must be a non-negative integer, not -10"):
            Signal(-10)

    def test_name(self):
        s1 = Signal()
        self.assertEqual(s1.name, "s1")
        s2 = Signal(name="sig")
        self.assertEqual(s2.name, "sig")

    def test_reset(self):
        s1 = Signal(4, reset=0b111, reset_less=True)
        self.assertEqual(s1.reset, 0b111)
        self.assertEqual(s1.reset_less, True)

    def test_reset_enum(self):
        s1 = Signal(2, reset=UnsignedEnum.BAR)
        self.assertEqual(s1.reset, 2)
        with self.assertRaises(TypeError,
                msg="Reset value has to be an int or an integral Enum"
        ):
            Signal(1, reset=StringEnum.FOO)

    def test_reset_narrow(self):
        with self.assertWarns(SyntaxWarning,
                msg="Reset value 8 requires 4 bits to represent, but the signal only has 3 bits"):
            Signal(3, reset=8)
        with self.assertWarns(SyntaxWarning,
                msg="Reset value 4 requires 4 bits to represent, but the signal only has 3 bits"):
            Signal(signed(3), reset=4)
        with self.assertWarns(SyntaxWarning,
                msg="Reset value -5 requires 4 bits to represent, but the signal only has 3 bits"):
            Signal(signed(3), reset=-5)

    def test_attrs(self):
        s1 = Signal()
        self.assertEqual(s1.attrs, {})
        s2 = Signal(attrs={"no_retiming": True})
        self.assertEqual(s2.attrs, {"no_retiming": True})

    def test_repr(self):
        s1 = Signal()
        self.assertEqual(repr(s1), "(sig s1)")

    def test_like(self):
        s1 = Signal.like(Signal(4))
        self.assertEqual(s1.shape(), unsigned(4))
        s2 = Signal.like(Signal(range(-15, 1)))
        self.assertEqual(s2.shape(), signed(5))
        s3 = Signal.like(Signal(4, reset=0b111, reset_less=True))
        self.assertEqual(s3.reset, 0b111)
        self.assertEqual(s3.reset_less, True)
        s4 = Signal.like(Signal(attrs={"no_retiming": True}))
        self.assertEqual(s4.attrs, {"no_retiming": True})
        s5 = Signal.like(Signal(decoder=str))
        self.assertEqual(s5.decoder, str)
        s6 = Signal.like(10)
        self.assertEqual(s6.shape(), unsigned(4))
        s7 = [Signal.like(Signal(4))][0]
        self.assertEqual(s7.name, "$like")
        s8 = Signal.like(s1, name_suffix="_ff")
        self.assertEqual(s8.name, "s1_ff")

    def test_decoder(self):
        class Color(Enum):
            RED  = 1
            BLUE = 2
        s = Signal(decoder=Color)
        self.assertEqual(s.decoder(1), "RED/1")
        self.assertEqual(s.decoder(3), "3")

    def test_enum(self):
        s1 = Signal(UnsignedEnum)
        self.assertEqual(s1.shape(), unsigned(2))
        s2 = Signal(SignedEnum)
        self.assertEqual(s2.shape(), signed(2))
        self.assertEqual(s2.decoder(SignedEnum.FOO), "FOO/-1")


class ClockSignalTestCase(FHDLTestCase):
    def test_domain(self):
        s1 = ClockSignal()
        self.assertEqual(s1.domain, "sync")
        s2 = ClockSignal("pix")
        self.assertEqual(s2.domain, "pix")

        with self.assertRaises(TypeError,
                msg="Clock domain name must be a string, not 1"):
            ClockSignal(1)

    def test_shape(self):
        s1 = ClockSignal()
        self.assertEqual(s1.shape(), unsigned(1))
        self.assertIsInstance(s1.shape(), Shape)

    def test_repr(self):
        s1 = ClockSignal()
        self.assertEqual(repr(s1), "(clk sync)")

    def test_wrong_name_comb(self):
        with self.assertRaises(ValueError,
                msg="Domain 'comb' does not have a clock"):
            ClockSignal("comb")


class ResetSignalTestCase(FHDLTestCase):
    def test_domain(self):
        s1 = ResetSignal()
        self.assertEqual(s1.domain, "sync")
        s2 = ResetSignal("pix")
        self.assertEqual(s2.domain, "pix")

        with self.assertRaises(TypeError,
                msg="Clock domain name must be a string, not 1"):
            ResetSignal(1)

    def test_shape(self):
        s1 = ResetSignal()
        self.assertEqual(s1.shape(), unsigned(1))
        self.assertIsInstance(s1.shape(), Shape)

    def test_repr(self):
        s1 = ResetSignal()
        self.assertEqual(repr(s1), "(rst sync)")

    def test_wrong_name_comb(self):
        with self.assertRaises(ValueError,
                msg="Domain 'comb' does not have a reset"):
            ResetSignal("comb")


class MockUserValue(UserValue):
    def __init__(self, lowered):
        super().__init__()
        self.lower_count = 0
        self.lowered     = lowered

    def lower(self):
        self.lower_count += 1
        return self.lowered


class UserValueTestCase(FHDLTestCase):
    def test_shape(self):
        uv = MockUserValue(1)
        self.assertEqual(uv.shape(), unsigned(1))
        self.assertIsInstance(uv.shape(), Shape)
        uv.lowered = 2
        self.assertEqual(uv.shape(), unsigned(1))
        self.assertEqual(uv.lower_count, 1)


class SampleTestCase(FHDLTestCase):
    def test_const(self):
        s = Sample(1, 1, "sync")
        self.assertEqual(s.shape(), unsigned(1))

    def test_signal(self):
        s1 = Sample(Signal(2), 1, "sync")
        self.assertEqual(s1.shape(), unsigned(2))
        s2 = Sample(ClockSignal(), 1, "sync")
        s3 = Sample(ResetSignal(), 1, "sync")

    def test_wrong_value_operator(self):
        with self.assertRaises(TypeError,
                "Sampled value must be a signal or a constant, not "
                "(+ (sig $signal) (const 1'd1))"):
            Sample(Signal() + 1, 1, "sync")

    def test_wrong_clocks_neg(self):
        with self.assertRaises(ValueError,
                "Cannot sample a value 1 cycles in the future"):
            Sample(Signal(), -1, "sync")

    def test_wrong_domain(self):
        with self.assertRaises(TypeError,
                "Domain name must be a string or None, not 0"):
            Sample(Signal(), 1, 0)


class InitialTestCase(FHDLTestCase):
    def test_initial(self):
        i = Initial()
        self.assertEqual(i.shape(), unsigned(1))
