"""
Tests for programmatic record creation.

Verifies that ``objects.create()``, ``object.save()``, and ``bulk_create()``
all correctly trigger the full bitemporal pipeline:
  Level 1:  History record with valid_from / valid_to
  Level 2:  MetaHistory record with sys_from / sys_to
  Main table synchronisation
"""

import datetime
from unittest.mock import patch

from django.db import connection, models
from django.test import TransactionTestCase

from lex.core.models.LexModel import LexModel
from lex.process_admin.utils.model_registration import ModelRegistration


# ── Test Model ──────────────────────────────────────────────────────
class ProgrammaticTestModel(LexModel):
    name = models.CharField(max_length=100)
    value = models.IntegerField(default=0)

    class Meta:
        app_label = "lex_app"


class ProgrammaticCreationTest(TransactionTestCase):
    """Verify history is created for various programmatic creation paths."""

    def setUp(self):
        from simple_history.models import registered_models

        mr = ModelRegistration()

        if ProgrammaticTestModel in registered_models:
            del registered_models[ProgrammaticTestModel]

        try:
            mr._register_standard_model(ProgrammaticTestModel, [])
        except Exception as e:
            print(f"Warning: Registration: {e}")

        self.HistoryModel = ProgrammaticTestModel.history.model
        self.MetaModel = self.HistoryModel.meta_history.model

        tables = connection.introspection.table_names()
        with connection.schema_editor() as schema_editor:
            for model in [ProgrammaticTestModel, self.HistoryModel, self.MetaModel]:
                if model._meta.db_table in tables:
                    schema_editor.delete_model(model)
                schema_editor.create_model(model)

    def tearDown(self):
        from simple_history.models import registered_models

        if ProgrammaticTestModel in registered_models:
            del registered_models[ProgrammaticTestModel]

        with connection.schema_editor() as schema_editor:
            for model in [self.MetaModel, self.HistoryModel, ProgrammaticTestModel]:
                try:
                    schema_editor.delete_model(model)
                except Exception:
                    pass

    # ────────────────────────────────────────────────────────────────
    # Test: objects.create()
    # ────────────────────────────────────────────────────────────────

    def test_create_generates_history(self):
        """objects.create() should produce 1 history + 1 meta-history record."""
        T0 = datetime.datetime(2025, 6, 1, 10, 0, 0)

        with patch("django.utils.timezone.now", return_value=T0):
            obj = ProgrammaticTestModel.objects.create(name="alpha", value=42)

        # ── Main table ──
        self.assertEqual(ProgrammaticTestModel.objects.count(), 1)
        self.assertEqual(ProgrammaticTestModel.objects.first().name, "alpha")

        # ── History (Level 1) ──
        h_records = list(self.HistoryModel.objects.filter(id=obj.pk))
        self.assertEqual(len(h_records), 1, "Expected exactly 1 history record")

        h = h_records[0]
        self.assertEqual(h.name, "alpha")
        self.assertEqual(h.value, 42)
        self.assertEqual(h.history_type, "+")
        self.assertTrue(abs((h.valid_from - T0).total_seconds()) < 1.0)
        self.assertIsNone(h.valid_to, "First record should have valid_to=None (∞)")

        # ── MetaHistory (Level 2) ──
        m_records = list(self.MetaModel.objects.filter(history_object=h))
        self.assertGreaterEqual(len(m_records), 1, "Expected at least 1 meta record")

        m = m_records[0]
        self.assertTrue(abs((m.sys_from - T0).total_seconds()) < 1.0)
        self.assertIsNone(m.sys_to, "First meta record should have sys_to=None")

    # ────────────────────────────────────────────────────────────────
    # Test: instance.save() (manual creation)
    # ────────────────────────────────────────────────────────────────

    def test_save_generates_history(self):
        """Manual obj = Model(); obj.save() should also produce history."""
        T0 = datetime.datetime(2025, 6, 1, 11, 0, 0)

        with patch("django.utils.timezone.now", return_value=T0):
            obj = ProgrammaticTestModel(name="beta", value=7)
            obj.save()

        # ── History ──
        h_records = list(self.HistoryModel.objects.filter(id=obj.pk))
        self.assertEqual(len(h_records), 1)
        self.assertEqual(h_records[0].name, "beta")
        self.assertEqual(h_records[0].history_type, "+")

        # ── Meta ──
        m_records = list(self.MetaModel.objects.filter(history_object=h_records[0]))
        self.assertGreaterEqual(len(m_records), 1)

    # ────────────────────────────────────────────────────────────────
    # Test: objects.create() then .save() (update)
    # ────────────────────────────────────────────────────────────────

    def test_update_generates_second_history(self):
        """Updating a record should create a second history entry of type '~'."""
        T0 = datetime.datetime(2025, 6, 1, 12, 0, 0)
        T1 = datetime.datetime(2025, 6, 1, 12, 5, 0)

        with patch("django.utils.timezone.now", return_value=T0):
            obj = ProgrammaticTestModel.objects.create(name="gamma", value=1)

        with patch("django.utils.timezone.now", return_value=T1):
            obj.name = "gamma_updated"
            obj.value = 2
            obj.save()

        h_records = list(
            self.HistoryModel.objects.filter(id=obj.pk).order_by("valid_from")
        )
        self.assertEqual(len(h_records), 2, "Expected 2 history records (create + update)")

        # First record: created
        self.assertEqual(h_records[0].name, "gamma")
        self.assertEqual(h_records[0].history_type, "+")
        self.assertTrue(abs((h_records[0].valid_from - T0).total_seconds()) < 1.0)
        # Chaining: valid_to of first = valid_from of second
        self.assertTrue(abs((h_records[0].valid_to - T1).total_seconds()) < 1.0)

        # Second record: updated
        self.assertEqual(h_records[1].name, "gamma_updated")
        self.assertEqual(h_records[1].history_type, "~")
        self.assertTrue(abs((h_records[1].valid_from - T1).total_seconds()) < 1.0)
        self.assertIsNone(h_records[1].valid_to, "Latest record should be open-ended")

    # ────────────────────────────────────────────────────────────────
    # Test: bulk_create()
    # ────────────────────────────────────────────────────────────────

    def test_bulk_create_generates_history(self):
        """
        LexManager.bulk_create() loops with save() inside transaction.atomic(),
        so each object should get full history + meta-history.
        """
        T0 = datetime.datetime(2025, 6, 1, 14, 0, 0)

        with patch("django.utils.timezone.now", return_value=T0):
            objs = ProgrammaticTestModel.objects.bulk_create([
                ProgrammaticTestModel(name="delta", value=10),
                ProgrammaticTestModel(name="epsilon", value=20),
                ProgrammaticTestModel(name="zeta", value=30),
            ])

        # ── Main table ──
        self.assertEqual(ProgrammaticTestModel.objects.count(), 3)

        # ── All 3 should have PKs assigned ──
        for obj in objs:
            self.assertIsNotNone(obj.pk, "bulk_create should return objects with PKs")

        # ── History (Level 1) — each object should have 1 record ──
        h_count = self.HistoryModel.objects.count()
        self.assertEqual(h_count, 3, "Each bulk-created object should have 1 history record")

        for obj in objs:
            h = self.HistoryModel.objects.filter(id=obj.pk)
            self.assertEqual(h.count(), 1, f"Object {obj.name} should have 1 history record")
            self.assertEqual(h.first().name, obj.name)
            self.assertEqual(h.first().history_type, "+")

        # ── MetaHistory (Level 2) — each history record should have a meta record ──
        m_count = self.MetaModel.objects.count()
        self.assertGreaterEqual(m_count, 3, "Each history record should have at least 1 meta record")

    def test_bulk_create_skip_history(self):
        """
        skip_history=True should use raw bulk_create without history.
        """
        T0 = datetime.datetime(2025, 6, 1, 14, 30, 0)

        with patch("django.utils.timezone.now", return_value=T0):
            objs = ProgrammaticTestModel.objects.bulk_create(
                [
                    ProgrammaticTestModel(name="raw_a", value=100),
                    ProgrammaticTestModel(name="raw_b", value=200),
                ],
                skip_history=True,
            )

        # Main table has the objects
        self.assertEqual(ProgrammaticTestModel.objects.count(), 2)

        # No history should exist (raw bulk_create bypasses signals)
        h_count = self.HistoryModel.objects.count()
        self.assertEqual(h_count, 0, "skip_history=True should NOT create history records")

    # ────────────────────────────────────────────────────────────────
    # Test: Multiple creates for same model
    # ────────────────────────────────────────────────────────────────

    def test_multiple_creates(self):
        """Creating multiple independent objects should each get their own history chain."""
        T0 = datetime.datetime(2025, 6, 1, 15, 0, 0)

        with patch("django.utils.timezone.now", return_value=T0):
            obj_a = ProgrammaticTestModel.objects.create(name="A", value=1)
            obj_b = ProgrammaticTestModel.objects.create(name="B", value=2)

        h_a = list(self.HistoryModel.objects.filter(id=obj_a.pk))
        h_b = list(self.HistoryModel.objects.filter(id=obj_b.pk))

        self.assertEqual(len(h_a), 1, "Object A should have 1 history record")
        self.assertEqual(len(h_b), 1, "Object B should have 1 history record")
        self.assertEqual(h_a[0].name, "A")
        self.assertEqual(h_b[0].name, "B")

        # Each should have independent meta records
        m_a = self.MetaModel.objects.filter(history_object=h_a[0]).count()
        m_b = self.MetaModel.objects.filter(history_object=h_b[0]).count()
        self.assertGreaterEqual(m_a, 1)
        self.assertGreaterEqual(m_b, 1)

    # ────────────────────────────────────────────────────────────────
    # Test: Delete creates history with type '-'
    # ────────────────────────────────────────────────────────────────

    def test_delete_generates_deletion_history(self):
        """Deleting an object should create a deletion marker in history."""
        T0 = datetime.datetime(2025, 6, 1, 16, 0, 0)
        T1 = datetime.datetime(2025, 6, 1, 16, 5, 0)

        with patch("django.utils.timezone.now", return_value=T0):
            obj = ProgrammaticTestModel.objects.create(name="to_delete", value=99)
        pk = obj.pk

        with patch("django.utils.timezone.now", return_value=T1):
            obj.delete()

        # Main table should be empty
        self.assertEqual(ProgrammaticTestModel.objects.filter(pk=pk).count(), 0)

        # History should contain both create (+) and delete (-) records
        h_records = list(
            self.HistoryModel.objects.filter(id=pk).order_by("valid_from", "history_id")
        )
        self.assertGreaterEqual(len(h_records), 2, "Expected at least create + delete records")

        types = [r.history_type for r in h_records]
        self.assertIn("+", types, "Should have a creation record")
        self.assertIn("-", types, "Should have a deletion record")
