# (c) cavaliba.com - tests / data / instance revision

import json

from django.test import TestCase

import app_home.cache as cache
from app_data.data import Instance
from app_data.models import DataInstance, DataRevision
from app_data.revision import revision_add, revision_add_raw
from app_data.user import User
from tests import helper

AAA = {"username": "testuser", "perms": ["p_data_admin"]}


class TestDiffRevision(TestCase):
    def setUp(self):
        cache.clear()
        helper.add_schema(
            classname="brtest",
            field_definition={"title": {"dataformat": "string", "displayname": "Title"}},
        )

    def test_diff_revision_empty_when_unbound(self):
        instance = Instance(classname="brtest")
        self.assertEqual(instance.diff_revision(), {})

    def test_diff_revision_empty_when_no_change(self):
        helper.add_instance(classname="brtest", keyname="obj04")
        instance = Instance.from_keyname("brtest", "obj04")
        instance.merge_import({"classname": "brtest", "keyname": "obj04"})
        self.assertEqual(instance.diff_revision(), {})

    def test_diff_revision_detects_displayname_change(self):
        helper.add_instance(classname="brtest", keyname="obj05", fields={"displayname": "Old"})
        instance = Instance.from_keyname("brtest", "obj05")
        instance.merge_import({"classname": "brtest", "keyname": "obj05", "displayname": "New"})
        diff = instance.diff_revision()
        self.assertIn("displayname", diff)
        self.assertEqual(diff["displayname"]["before"], "Old")
        self.assertEqual(diff["displayname"]["after"], "New")

    def test_diff_revision_detects_is_enabled_change(self):
        helper.add_instance(classname="brtest", keyname="obj06")
        instance = Instance.from_keyname("brtest", "obj06")
        instance.merge_import({"classname": "brtest", "keyname": "obj06", "is_enabled": "false"})
        diff = instance.diff_revision()
        self.assertIn("is_enabled", diff)


class TestAaaRevision(TestCase):
    AAA = {"username": "apiuser", "perms": ["p_data_admin"]}

    def setUp(self):
        cache.clear()
        helper.add_schema(classname="aatest")

    def test_aaa_revision_empty_on_init(self):
        instance = Instance(classname="aatest")
        self.assertEqual(instance.aaa_revision, {})

    def test_aaa_revision_set_by_save(self):
        # aaa is no longer stashed by merge_import()/merge_request() (they only
        # use it for permission-gated fields) — save() is the single place
        # that sets self.aaa_revision, via diff_revision(aaa=aaa)
        instance = Instance(classname="aatest")
        instance.merge_import({"classname": "aatest", "keyname": "obj01"})
        self.assertEqual(instance.aaa_revision, {})
        instance.save(aaa=self.AAA, action="create")
        self.assertEqual(instance.aaa_revision.get("username"), "apiuser")

    def test_aaa_revision_not_in_data_json(self):
        instance = Instance(classname="aatest")
        instance.merge_import({"classname": "aatest", "keyname": "obj03"})
        instance.save(aaa=self.AAA, action="create")
        iobj = DataInstance.objects.get(classname="aatest", keyname="obj03")
        self.assertNotIn("aaa_revision", iobj.data_json)

    def test_revision_username_from_aaa_revision(self):
        instance = Instance(classname="aatest")
        instance.merge_import({"classname": "aatest", "keyname": "obj04"})
        instance.save(aaa=self.AAA, action="create")
        rev = DataRevision.objects.filter(classname="aatest", keyname="obj04").first()
        self.assertIsNotNone(rev)
        self.assertEqual(rev.username, "apiuser")

    def test_no_revision_without_aaa_revision(self):
        instance = Instance(classname="aatest")
        instance.merge_import({"classname": "aatest", "keyname": "obj05"})
        instance.save(action="create")
        self.assertEqual(
            DataRevision.objects.filter(classname="aatest", keyname="obj05").count(), 0
        )


class TestInstanceDiffRevisionBlankString(TestCase):
    def setUp(self):
        cache.clear()
        helper.add_schema(
            classname="brblank",
            field_definition={
                "note": {"dataformat": "text", "displayname": "Note"},
                "title": {"dataformat": "string", "displayname": "Title"},
            },
        )

    def test_diff_revision_ignores_none_to_empty_string_text(self):
        helper.add_instance(classname="brblank", keyname="obj01", fields={"note": None})
        instance = Instance.from_keyname("brblank", "obj01")
        instance.merge_import({"classname": "brblank", "keyname": "obj01", "note": ""})
        diff = instance.diff_revision()
        self.assertNotIn("note", diff.get("fields", {}))

    def test_diff_revision_ignores_empty_string_to_none_text(self):
        helper.add_instance(classname="brblank", keyname="obj02", fields={"note": ""})
        instance = Instance.from_keyname("brblank", "obj02")
        instance.merge_import({"classname": "brblank", "keyname": "obj02", "note": None})
        diff = instance.diff_revision()
        self.assertNotIn("note", diff.get("fields", {}))

    def test_diff_revision_detects_real_text_change(self):
        helper.add_instance(classname="brblank", keyname="obj03", fields={"note": "hello"})
        instance = Instance.from_keyname("brblank", "obj03")
        instance.merge_import({"classname": "brblank", "keyname": "obj03", "note": "world"})
        diff = instance.diff_revision()
        self.assertIn("note", diff.get("fields", {}))
        self.assertEqual(diff["fields"]["note"]["before"], ["hello"])
        self.assertEqual(diff["fields"]["note"]["after"], ["world"])

    def test_diff_revision_ignores_none_to_empty_string_title(self):
        helper.add_instance(classname="brblank", keyname="obj04", fields={"title": None})
        instance = Instance.from_keyname("brblank", "obj04")
        instance.merge_import({"classname": "brblank", "keyname": "obj04", "title": ""})
        diff = instance.diff_revision()
        self.assertNotIn("title", diff.get("fields", {}))


class TestInstanceDiffRevisionExcludedFields(TestCase):
    def setUp(self):
        cache.clear()

    def test_last_login_change_alone_produces_no_diff(self):
        helper.add_instance(
            classname="user", keyname="alice", fields={"last_login": "2026-01-01 10:00:00"}
        )
        instance = User.from_keyname(classname="user", keyname="alice")
        instance.merge_import(
            {"classname": "user", "keyname": "alice", "last_login": "2026-07-07 09:00:00"},
            aaa=AAA,
        )
        diff = instance.diff_revision()
        self.assertEqual(diff.get("fields", {}), {})

    def test_last_login_excluded_but_other_field_change_still_detected(self):
        helper.add_instance(
            classname="user",
            keyname="bob",
            fields={"last_login": "2026-01-01 10:00:00", "firstname": "Old"},
        )
        instance = User.from_keyname(classname="user", keyname="bob")
        instance.merge_import(
            {
                "classname": "user",
                "keyname": "bob",
                "last_login": "2026-07-07 09:00:00",
                "firstname": "New",
            },
            aaa=AAA,
        )
        diff = instance.diff_revision()
        self.assertNotIn("last_login", diff.get("fields", {}))
        self.assertIn("firstname", diff.get("fields", {}))

    def test_field_named_last_sync_is_excluded_for_any_schema(self):
        helper.add_schema(
            classname="brexclude",
            field_definition={"last_sync": {"dataformat": "string", "displayname": "Last Sync"}},
        )
        helper.add_instance(classname="brexclude", keyname="obj01", fields={"last_sync": "old"})
        instance = Instance.from_keyname("brexclude", "obj01")
        instance.merge_import({"classname": "brexclude", "keyname": "obj01", "last_sync": "new"})
        diff = instance.diff_revision()
        self.assertEqual(diff.get("fields", {}), {})

    def test_field_named_last_update_is_excluded_for_any_schema(self):
        helper.add_schema(
            classname="brexclude2",
            field_definition={
                "last_update": {"dataformat": "string", "displayname": "Last Update"}
            },
        )
        helper.add_instance(classname="brexclude2", keyname="obj01", fields={"last_update": "old"})
        instance = Instance.from_keyname("brexclude2", "obj01")
        instance.merge_import({"classname": "brexclude2", "keyname": "obj01", "last_update": "new"})
        diff = instance.diff_revision()
        self.assertEqual(diff.get("fields", {}), {})


class TestEnableDisableRevision(TestCase):
    def setUp(self):
        cache.clear()
        helper.add_schema(classname="edtest")

    def test_enable_disabled_instance_adds_revision(self):
        helper.add_instance(classname="edtest", keyname="obj01", fields={"is_enabled": "false"})
        instance = Instance.from_keyname("edtest", "obj01")
        r = instance.enable(aaa=AAA)
        self.assertTrue(r)
        self.assertEqual(
            DataRevision.objects.filter(
                classname="edtest", keyname="obj01", action="enable"
            ).count(),
            1,
        )

    def test_enable_already_enabled_no_revision(self):
        helper.add_instance(classname="edtest", keyname="obj02")
        instance = Instance.from_keyname("edtest", "obj02")
        self.assertTrue(instance.is_enabled)
        r = instance.enable(aaa=AAA)
        self.assertTrue(r)
        self.assertEqual(
            DataRevision.objects.filter(
                classname="edtest", keyname="obj02", action="enable"
            ).count(),
            0,
        )

    def test_disable_enabled_instance_adds_revision(self):
        helper.add_instance(classname="edtest", keyname="obj03")
        instance = Instance.from_keyname("edtest", "obj03")
        self.assertTrue(instance.is_enabled)
        r = instance.disable(aaa=AAA)
        self.assertTrue(r)
        self.assertEqual(
            DataRevision.objects.filter(
                classname="edtest", keyname="obj03", action="disable"
            ).count(),
            1,
        )

    def test_disable_already_disabled_no_revision(self):
        helper.add_instance(classname="edtest", keyname="obj04", fields={"is_enabled": "false"})
        instance = Instance.from_keyname("edtest", "obj04")
        r = instance.disable(aaa=AAA)
        self.assertTrue(r)
        self.assertEqual(
            DataRevision.objects.filter(
                classname="edtest", keyname="obj04", action="disable"
            ).count(),
            0,
        )

    def test_enable_with_aaa_sets_username_in_revision(self):
        helper.add_instance(classname="edtest", keyname="obj05", fields={"is_enabled": "false"})
        instance = Instance.from_keyname("edtest", "obj05")
        instance.enable(aaa={"username": "syncuser", "perms": []})
        rev = DataRevision.objects.filter(
            classname="edtest", keyname="obj05", action="enable"
        ).first()
        self.assertIsNotNone(rev)
        self.assertEqual(rev.username, "syncuser")


class TestRevisionDiff(TestCase):
    AAA = {"username": "testuser"}

    def setUp(self):
        cache.clear()
        helper.add_schema(classname="rdtest")
        helper.add_instance(classname="rdtest", keyname="obj01")

    def test_revision_add_with_diff_stores_json(self):
        diff = {"displayname": {"before": "Old", "after": "New"}}
        revision_add_raw(aaa=self.AAA, classname="rdtest", keyname="obj01", action="edit")

        instance = Instance.from_keyname("rdtest", "obj01")
        revision_add(aaa=self.AAA, instance=instance, action="edit", diff=diff)
        rev = DataRevision.objects.filter(classname="rdtest", keyname="obj01", action="edit").last()
        self.assertIsNotNone(rev)
        self.assertNotEqual(rev.data_diff, "")
        parsed = json.loads(rev.data_diff)
        self.assertIn("displayname", parsed)

    def test_revision_add_without_diff_stores_empty(self):
        instance = Instance.from_keyname("rdtest", "obj01")
        revision_add(aaa=self.AAA, instance=instance, action="edit")
        rev = DataRevision.objects.filter(classname="rdtest", keyname="obj01", action="edit").last()
        self.assertIsNotNone(rev)
        self.assertEqual(rev.data_diff, "")

    def test_revision_add_raw_data_diff_empty(self):
        revision_add_raw(aaa=self.AAA, classname="rdtest", keyname="obj01", action="test")
        rev = DataRevision.objects.filter(
            classname="rdtest", keyname="obj01", action="test"
        ).first()
        self.assertIsNotNone(rev)
        self.assertEqual(rev.data_diff, "")


class TestSkipRevision(TestCase):
    def setUp(self):
        cache.clear()
        helper.add_schema(classname="srtest")

    def test_skip_revision_true_writes_but_skips_revision(self):
        helper.add_instance(classname="srtest", keyname="obj01", fields={"displayname": "Old"})
        instance = Instance.from_keyname("srtest", "obj01")
        instance.displayname = "New"
        r = instance.save(aaa=AAA, action="update", skip_revision=True)
        self.assertTrue(r)

        reloaded = Instance.from_keyname("srtest", "obj01")
        self.assertEqual(reloaded.displayname, "New")
        self.assertEqual(
            DataRevision.objects.filter(classname="srtest", keyname="obj01").count(), 0
        )

    def test_skip_revision_false_default_still_creates_revision(self):
        helper.add_instance(classname="srtest", keyname="obj02", fields={"displayname": "Old"})
        instance = Instance.from_keyname("srtest", "obj02")
        instance.displayname = "New"
        instance.save(aaa=AAA, action="update")

        self.assertEqual(
            DataRevision.objects.filter(classname="srtest", keyname="obj02").count(), 1
        )

    def test_force_with_no_diff_creates_no_revision(self):
        helper.add_instance(classname="srtest", keyname="obj03")
        instance = Instance.from_keyname("srtest", "obj03")
        instance.save(aaa=AAA, action="update", force=True)

        self.assertEqual(
            DataRevision.objects.filter(classname="srtest", keyname="obj03").count(), 0
        )
