import copy
import datetime as dt
import unittest

from hippolyzer.lib.base.datatypes import *
from hippolyzer.lib.base.inventory import InventoryModel, SaleType, InventoryItem
from hippolyzer.lib.base.message.message import Block, Message
from hippolyzer.lib.base.wearables import Wearable, VISUAL_PARAMS

SIMPLE_INV = """\tinv_object\t0
\t{
\t\tobj_id\tf4d91477-def1-487a-b4f3-6fa201c17376
\t\tparent_id\t00000000-0000-0000-0000-000000000000
\t\ttype\tcategory
\t\tname\tContents|
\t\tmetadata\t<llsd><undef /></llsd>
|
\t}
\tinv_item\t0
\t{
\t\titem_id\tdd163122-946b-44df-99f6-a6030e2b9597
\t\tparent_id\tf4d91477-def1-487a-b4f3-6fa201c17376
\tpermissions 0
\t{
\t\tbase_mask\t7fffffff
\t\towner_mask\t7fffffff
\t\tgroup_mask\t00000000
\t\teveryone_mask\t00000000
\t\tnext_owner_mask\t0008e000
\t\tcreator_id\ta2e76fcd-9360-4f6d-a924-000000000003
\t\towner_id\ta2e76fcd-9360-4f6d-a924-000000000003
\t\tlast_owner_id\ta2e76fcd-9360-4f6d-a924-000000000003
\t\tgroup_id\t00000000-0000-0000-0000-000000000000
\t}
\t\tasset_id\t00000000-0000-0000-0000-000000000000
\t\ttype\tlsltext
\t\tinv_type\tscript
\t\tflags\t00000000
\tsale_info\t0
\t{
\t\tsale_type\tnot
\t\tsale_price\t10
\t}
\t\tname\tNew Script|
\t\tdesc\t2020-04-20 04:20:39 lsl2 script|
\t\tmetadata\t<llsd><map><key>experience</key><uuid>a2e76fcd-9360-4f6d-a924-000000000003</uuid></map></llsd>
|
\t\tcreation_date\t1587367239
\t}
"""

SIMPLE_INV_PARSED = [
    {
        'name': 'Contents',
        'obj_id': UUID('f4d91477-def1-487a-b4f3-6fa201c17376'),
        'parent_id': UUID('00000000-0000-0000-0000-000000000000'),
        'type': 'category'
    },
    {
        'asset_id': UUID('00000000-0000-0000-0000-000000000000'),
        'created_at': 1587367239,
        'desc': '2020-04-20 04:20:39 lsl2 script',
        'flags': b'\x00\x00\x00\x00',
        'inv_type': 'script',
        'item_id': UUID('dd163122-946b-44df-99f6-a6030e2b9597'),
        'name': 'New Script',
        'metadata': {"experience": UUID("a2e76fcd-9360-4f6d-a924-000000000003")},
        'parent_id': UUID('f4d91477-def1-487a-b4f3-6fa201c17376'),
        'permissions': {
            'base_mask': 2147483647,
            'creator_id': UUID('a2e76fcd-9360-4f6d-a924-000000000003'),
            'everyone_mask': 0,
            'group_id': UUID('00000000-0000-0000-0000-000000000000'),
            'group_mask': 0,
            'last_owner_id': UUID('a2e76fcd-9360-4f6d-a924-000000000003'),
            'next_owner_mask': 581632,
            'owner_id': UUID('a2e76fcd-9360-4f6d-a924-000000000003'),
            'owner_mask': 2147483647,
        },
        'sale_info': {
            'sale_price': 10,
            'sale_type': 'not'
        },
        'type': 'lsltext'
    }
]

INV_CATEGORY = """\tinv_category\t0
\t{
\t\tcat_id\tf4d91477-def1-487a-b4f3-6fa201c17376
\t\tparent_id\t00000000-0000-0000-0000-000000000000
\t\ttype\tlsltext
\t\tpref_type\tlsltext
\t\tname\tScripts|
\t\towner_id\ta2e76fcd-9360-4f6d-a924-000000000003
\t}
"""


class TestLegacyInv(unittest.TestCase):
    def setUp(self) -> None:
        self.model = InventoryModel.from_str(SIMPLE_INV)

    def test_parse(self):
        self.assertTrue(UUID('f4d91477-def1-487a-b4f3-6fa201c17376') in self.model.nodes)
        self.assertIsNotNone(self.model.root)

    def test_parse_category(self):
        model = InventoryModel.from_str(INV_CATEGORY)
        self.assertEqual(UUID('f4d91477-def1-487a-b4f3-6fa201c17376'), model.root.node_id)

    def test_serialize(self):
        new_model = InventoryModel.from_str(self.model.to_str())
        self.assertEqual(self.model, new_model)

    def test_serialize_category(self):
        model = InventoryModel.from_str(INV_CATEGORY)
        new_model = InventoryModel.from_str(model.to_str())
        self.assertEqual(model, new_model)

    def test_category_legacy_serialization(self):
        self.assertEqual(INV_CATEGORY, InventoryModel.from_str(INV_CATEGORY).to_str())

    def test_item_access(self):
        item = self.model.nodes[UUID('dd163122-946b-44df-99f6-a6030e2b9597')]
        self.assertEqual(item.name, "New Script")
        self.assertEqual(item.sale_info.sale_type, SaleType.NOT)
        self.assertDictEqual(item.metadata, {"experience": UUID("a2e76fcd-9360-4f6d-a924-000000000003")})
        self.assertEqual(item.model, self.model)

    def test_access_children(self):
        root = self.model.root
        item = tuple(self.model.ordered_nodes)[1]
        self.assertEqual((item,), root.children)

    def test_access_parent(self):
        root = self.model.root
        item = tuple(self.model.ordered_nodes)[1]
        self.assertEqual(root, item.parent)
        self.assertEqual(None, root.parent)

    def test_unlink(self):
        self.assertEqual(1, len(self.model.root.children))
        item = tuple(self.model.ordered_nodes)[1]
        self.assertEqual([item], item.unlink())
        self.assertEqual(0, len(self.model.root.children))
        self.assertEqual(None, item.model)

    def test_relink(self):
        item = tuple(self.model.ordered_nodes)[1]
        for unlinked in item.unlink():
            self.model.add(unlinked)
        self.assertEqual(self.model, item.model)
        self.assertEqual(1, len(self.model.root.children))

    def test_eq_excludes_model(self):
        item = tuple(self.model.ordered_nodes)[1]
        item_copy = copy.copy(item)
        item_copy.model = None
        self.assertEqual(item, item_copy)

    def test_llsd_serialization(self):
        self.assertEqual(self.model.to_llsd(), SIMPLE_INV_PARSED)

    def test_llsd_date_parsing(self):
        model = InventoryModel.from_llsd(SIMPLE_INV_PARSED)
        item: InventoryItem = model.nodes.get(UUID("dd163122-946b-44df-99f6-a6030e2b9597"))  # type: ignore
        self.assertEqual(item.creation_date, dt.datetime(2020, 4, 20, 7, 20, 39, tzinfo=dt.timezone.utc))

    def test_llsd_serialization_ais(self):
        model = InventoryModel.from_str(INV_CATEGORY)
        self.assertEqual(
            [
                {
                    'agent_id': UUID('a2e76fcd-9360-4f6d-a924-000000000003'),
                    'category_id': UUID('f4d91477-def1-487a-b4f3-6fa201c17376'),
                    'name': 'Scripts',
                    'parent_id': UUID('00000000-0000-0000-0000-000000000000'),
                    'type_default': 10,
                    'version': -1
                }
            ],
            model.to_llsd("ais")
        )

    def test_llsd_legacy_equality(self):
        new_model = InventoryModel.from_llsd(self.model.to_llsd())
        self.assertEqual(self.model, new_model)
        new_model.root.name = "foo"
        self.assertNotEqual(self.model, new_model)

    def test_legacy_serialization(self):
        self.assertEqual(SIMPLE_INV, self.model.to_str())

    def test_difference_added(self):
        new_model = InventoryModel.from_llsd(self.model.to_llsd())
        diff = self.model.get_differences(new_model)
        self.assertEqual([], diff.changed)
        self.assertEqual([], diff.removed)

        new_model.root.name = "foo"
        diff = self.model.get_differences(new_model)
        self.assertEqual([new_model.root], diff.changed)
        self.assertEqual([], diff.removed)

        item = new_model.root.children[0]
        item.unlink()
        diff = self.model.get_differences(new_model)
        self.assertEqual([new_model.root], diff.changed)
        self.assertEqual([item], diff.removed)

        new_item = copy.copy(item)
        new_item.node_id = UUID.random()
        new_model.add(new_item)
        diff = self.model.get_differences(new_model)
        self.assertEqual([new_model.root, new_item], diff.changed)
        self.assertEqual([item], diff.removed)


GIRL_NEXT_DOOR_SHAPE = """LLWearable version 22
Girl Next Door - C2 - med - Adam n Eve

\tpermissions 0
\t{
\t\tbase_mask\t0008e000
\t\towner_mask\t0008e000
\t\tgroup_mask\t00000000
\t\teveryone_mask\t00000000
\t\tnext_owner_mask\t0008e000
\t\tcreator_id\tdf110c10-72e6-40d6-8916-14b4b0366b18
\t\towner_id\t285a8af7-7a1d-4608-8fb5-200b816e7ee1
\t\tlast_owner_id\t285a8af7-7a1d-4608-8fb5-200b816e7ee1
\t\tgroup_id\t00000000-0000-0000-0000-000000000000
\t}
\tsale_info\t0
\t{
\t\tsale_type\tnot
\t\tsale_price\t10
\t}
type 0
parameters 82
1 -.21
2 -.4
4 -.08
5 .04
6 -.04
7 .02
8 -.42
10 -.15
11 .16
12 -.5
13 0
14 1
15 -.16
17 .17
18 -.42
19 -.78
20 -.16
21 .01
22 0
23 -.44
24 .32
25 .35
27 -.55
33 -.11
34 .22
35 -.1
36 -1.06
37 -.86
38 -.42
80 0
105 .59
155 -.24
157 0
185 -.68
193 .55
196 -.98
505 .39
506 -.68
507 -.77
515 -1
517 -.11
518 .6
629 .5
637 .24
646 -.26
647 -.05
649 .36
650 .25
652 .4
653 .29
656 0
659 .76
662 .5
663 0
664 0
665 .08
675 -.14
676 -.58
678 .5
682 .5
683 -.17
684 .28
685 0
690 .42
692 .26
693 -.12
753 .51
756 -.2
758 .21
759 .5
760 .85
764 .32
765 .18
769 .37
773 .53
795 .22
796 0
799 .5
841 -.12
842 0
879 0
880 -.37
textures 0
"""

# TODO: Move appearance-related stuff elsewhere.

GIRL_NEXT_DOOR_APPEARANCE_MSG = Message(
    'AvatarAppearance',
    Block('Sender', ID=UUID(int=1), IsTrial=0),
    # We don't care about the value of this.
    Block('ObjectData', TextureEntry=b""),
    Block('VisualParam', ParamValue=9),
    Block('VisualParam', ParamValue=30),
    Block('VisualParam', ParamValue=71),
    Block('VisualParam', ParamValue=32),
    Block('VisualParam', ParamValue=51),
    Block('VisualParam', ParamValue=132),
    Block('VisualParam', ParamValue=10),
    Block('VisualParam', ParamValue=76),
    Block('VisualParam', ParamValue=84),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=43),
    Block('VisualParam', ParamValue=83),
    Block('VisualParam', ParamValue=113),
    Block('VisualParam', ParamValue=68),
    Block('VisualParam', ParamValue=73),
    Block('VisualParam', ParamValue=43),
    Block('VisualParam', ParamValue=35),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=7),
    Block('VisualParam', ParamValue=132),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=76),
    Block('VisualParam', ParamValue=91),
    Block('VisualParam', ParamValue=129),
    Block('VisualParam', ParamValue=106),
    Block('VisualParam', ParamValue=76),
    Block('VisualParam', ParamValue=58),
    Block('VisualParam', ParamValue=99),
    Block('VisualParam', ParamValue=73),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=203),
    Block('VisualParam', ParamValue=48),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=150),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=114),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=76),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=40),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=140),
    Block('VisualParam', ParamValue=86),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=99),
    Block('VisualParam', ParamValue=84),
    Block('VisualParam', ParamValue=53),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=66),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=100),
    Block('VisualParam', ParamValue=216),
    Block('VisualParam', ParamValue=214),
    Block('VisualParam', ParamValue=204),
    Block('VisualParam', ParamValue=204),
    Block('VisualParam', ParamValue=204),
    Block('VisualParam', ParamValue=51),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=89),
    Block('VisualParam', ParamValue=109),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=61),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=115),
    Block('VisualParam', ParamValue=76),
    Block('VisualParam', ParamValue=91),
    Block('VisualParam', ParamValue=158),
    Block('VisualParam', ParamValue=102),
    Block('VisualParam', ParamValue=109),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=193),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=132),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=68),
    Block('VisualParam', ParamValue=35),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=97),
    Block('VisualParam', ParamValue=92),
    Block('VisualParam', ParamValue=79),
    Block('VisualParam', ParamValue=107),
    Block('VisualParam', ParamValue=160),
    Block('VisualParam', ParamValue=112),
    Block('VisualParam', ParamValue=63),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=159),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=73),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=102),
    Block('VisualParam', ParamValue=158),
    Block('VisualParam', ParamValue=145),
    Block('VisualParam', ParamValue=153),
    Block('VisualParam', ParamValue=163),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=122),
    Block('VisualParam', ParamValue=43),
    Block('VisualParam', ParamValue=94),
    Block('VisualParam', ParamValue=135),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=214),
    Block('VisualParam', ParamValue=204),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=56),
    Block('VisualParam', ParamValue=30),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=204),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=112),
    Block('VisualParam', ParamValue=127),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=25),
    Block('VisualParam', ParamValue=100),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=84),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=51),
    Block('VisualParam', ParamValue=94),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=255),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=25),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=25),
    Block('VisualParam', ParamValue=23),
    Block('VisualParam', ParamValue=51),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=25),
    Block('VisualParam', ParamValue=23),
    Block('VisualParam', ParamValue=51),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=25),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=25),
    Block('VisualParam', ParamValue=23),
    Block('VisualParam', ParamValue=51),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=25),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=25),
    Block('VisualParam', ParamValue=23),
    Block('VisualParam', ParamValue=51),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=25),
    Block('VisualParam', ParamValue=23),
    Block('VisualParam', ParamValue=51),
    Block('VisualParam', ParamValue=0),
    Block('VisualParam', ParamValue=25),
    Block('VisualParam', ParamValue=23),
    Block('VisualParam', ParamValue=51),
    Block('VisualParam', ParamValue=1),
    Block('VisualParam', ParamValue=127),
    Block('AppearanceData', AppearanceVersion=1, CofVersion=100, Flags=0),
    Block('AppearanceHover', HoverHeight=Vector3(0.0, 0.0, 0.0))
)


class TestWearable(unittest.TestCase):
    def test_parse(self):
        wearable = Wearable.from_str(GIRL_NEXT_DOOR_SHAPE)
        self.assertEqual(wearable.name, "Girl Next Door - C2 - med - Adam n Eve")
        self.assertEqual(wearable.parameters[841], -.12)

    def test_serialize(self):
        wearable = Wearable.from_str(GIRL_NEXT_DOOR_SHAPE)
        new_wearable = Wearable.from_str(wearable.to_str())
        self.assertEqual(wearable, new_wearable)

    def test_visual_params(self):
        param = VISUAL_PARAMS.by_name("Eyelid_Inner_Corner_Up")
        self.assertEqual(param.value_max, 1.2)

    def test_message_equivalent(self):
        wearable = Wearable.from_str(GIRL_NEXT_DOOR_SHAPE)
        parsed = VISUAL_PARAMS.parse_appearance_message(GIRL_NEXT_DOOR_APPEARANCE_MSG)

        for i, (param_id, param_val) in enumerate(parsed.items()):
            param = VISUAL_PARAMS.by_id(param_id)
            if param.wearable != "shape":
                continue
            # A parameter may legitimately be missing from the shape depending on its age,
            # just assume it's the default value.
            expected_val = wearable.parameters.get(param_id, param.value_default)
            # This seems like quite a large delta. Maybe we should be using different quantization here.
            self.assertAlmostEqual(expected_val, param_val, delta=0.015)
