import unittest
from decimal import Decimal
import json

from strict_response_compare import compare


class StrictResponseCompareTest(unittest.TestCase):
    def test_equal_object_key_order_is_irrelevant(self):
        self.assertEqual([], compare({"a": 1, "b": [2, 3]}, {"b": [2, 3], "a": 1}))

    def test_missing_field_fails(self):
        self.assertEqual('$["data"]["id"]', compare({"data": {"id": 1}}, {"data": {}})[0][0])

    def test_boolean_number_and_number_string_fail(self):
        self.assertTrue(compare(True, 1))
        self.assertTrue(compare(123, "123"))

    def test_null_missing_and_empty_container_are_distinct(self):
        self.assertTrue(compare({"x": None}, {}))
        self.assertTrue(compare([], {}))
        self.assertTrue(compare(None, ""))

    def test_decimal_difference_fails_without_float_rounding(self):
        self.assertTrue(compare("100.00", "100"))
        self.assertTrue(compare({"amount": "0.10"}, {"amount": "0.1"}))
        baseline = json.loads('{"amount":0.1}', parse_float=Decimal)
        changed = json.loads('{"amount":0.10000000000000001}', parse_float=Decimal)
        self.assertTrue(compare(baseline, changed))
        self.assertTrue(compare(9007199254740993, 9007199254740992))

    def test_array_reordering_and_duplicate_loss_fail(self):
        self.assertTrue(compare([1, 2], [2, 1]))
        self.assertTrue(compare([1, 1, 2], [1, 2]))

    def test_only_explicit_dynamic_path_is_normalized_after_validation(self):
        rules = {'$["trace_id"]': {"type": "string", "pattern": "[0-9a-f]{13}",
                                     "require_distinct": True, "reason": "per-request trace"}}
        expected = {"trace_id": "6abc708f89e8b", "amount": "1.00"}
        actual = {"trace_id": "6abc70906c0fe", "amount": "1.00"}
        self.assertEqual([], compare(expected, actual, rules=rules))
        self.assertTrue(compare(expected, {**actual, "trace_id": "bad"}, rules=rules))
        self.assertTrue(compare(expected, {**actual, "trace_id": 1}, rules=rules))
        self.assertTrue(compare(expected, {**actual, "trace_id": expected["trace_id"]}, rules=rules))
        self.assertTrue(compare(expected, {**actual, "amount": "1.0"}, rules=rules))

    def test_global_or_unjustified_ignore_rule_is_rejected(self):
        with self.assertRaises(ValueError):
            compare({"id": 1}, {"id": 2}, rules={"$": {"type": "object"}})
        with self.assertRaises(ValueError):
            compare({"amount": "1.00"}, {"amount": "2.00"},
                    rules={'$["amount"]': {"type": "string", "pattern": ".*",
                                               "reason": "invalid broad amount ignore"}})


if __name__ == "__main__":
    unittest.main()
