from __future__ import annotations

import unittest

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from govoplan_core.core.change_sequence import (
    ChangeSequenceEntry,
    ChangeSequenceRetentionFloor,
    decode_sequence_watermark,
    encode_sequence_watermark,
    max_sequence_id,
    min_sequence_id,
    prune_sequence_entries,
    record_change,
    retained_sequence_floor,
    sequence_entries_since,
    sequence_watermark_is_expired,
)
from govoplan_core.core.events import EventActorRef, EventBus, EventObjectRef, EventTenantRef, PlatformEvent, event_bus_context
from govoplan_core.core.runtime import (
    clear_runtime,
    configure_runtime,
    get_runtime_context,
)
from govoplan_core.db.base import Base


class ChangeSequenceTests(unittest.TestCase):
    def setUp(self) -> None:
        self._runtime_context = get_runtime_context()
        clear_runtime()

    def tearDown(self) -> None:
        if self._runtime_context is not None:
            configure_runtime(self._runtime_context)
        else:
            clear_runtime()

    def test_records_changes_and_returns_entries_after_watermark(self) -> None:
        engine = create_engine("sqlite:///:memory:")
        SessionLocal = sessionmaker(bind=engine)
        try:
            Base.metadata.create_all(bind=engine, tables=[ChangeSequenceEntry.__table__, ChangeSequenceRetentionFloor.__table__])
            with SessionLocal() as session:
                first = record_change(
                    session,
                    tenant_id="tenant-1",
                    module_id="files",
                    collection="files.assets",
                    resource_type="file",
                    resource_id="file-1",
                    operation="created",
                )
                second = record_change(
                    session,
                    tenant_id="tenant-1",
                    module_id="files",
                    collection="files.assets",
                    resource_type="file",
                    resource_id="file-1",
                    operation="updated",
                )
                session.commit()

                self.assertEqual(encode_sequence_watermark(first.id), "seq:1")
                self.assertEqual(decode_sequence_watermark("seq:1"), 1)
                self.assertEqual(max_sequence_id(session, tenant_id="tenant-1", module_id="files"), second.id)
                self.assertEqual(min_sequence_id(session, tenant_id="tenant-1", module_id="files"), first.id)
                self.assertFalse(sequence_watermark_is_expired(session, tenant_id="tenant-1", module_id="files", since=first.id - 1))
                self.assertEqual(
                    [entry.operation for entry in sequence_entries_since(session, tenant_id="tenant-1", module_id="files", since=first.id)],
                    ["updated"],
                )

                session.delete(first)
                session.commit()
                self.assertFalse(sequence_watermark_is_expired(session, tenant_id="tenant-1", module_id="files", since=0))
        finally:
            engine.dispose()

    def test_record_change_publishes_module_change_event(self) -> None:
        engine = create_engine("sqlite:///:memory:")
        SessionLocal = sessionmaker(bind=engine)
        try:
            Base.metadata.create_all(bind=engine, tables=[ChangeSequenceEntry.__table__, ChangeSequenceRetentionFloor.__table__])
            seen: list[PlatformEvent] = []
            bus = EventBus()
            bus.subscribe("*", seen.append)
            cases = (
                ("files", "files.assets", "file", "file-1", "created", "files.file.created"),
                ("mail", "mail.profiles", "mail_profile", "profile-1", "updated", "mail.profile.updated"),
                ("campaigns", "campaigns.jobs", "campaign_job", "job-1", "updated", "campaigns.campaign.job.updated"),
            )
            with SessionLocal() as session:
                with event_bus_context(bus):
                    for module_id, collection, resource_type, resource_id, operation, _event_type in cases:
                        record_change(
                            session,
                            tenant_id="tenant-1",
                            module_id=module_id,
                            collection=collection,
                            resource_type=resource_type,
                            resource_id=resource_id,
                            operation=operation,
                            actor_type="user",
                            actor_id="user-1",
                            payload={"scope_type": "tenant"},
                        )
                    self.assertEqual([], seen)
                    session.commit()

            self.assertEqual([case[-1] for case in cases], [event.type for event in seen])
            event = seen[1]
            self.assertEqual("mail", event.module_id)
            self.assertEqual(EventActorRef(type="user", id="user-1"), event.actor)
            self.assertEqual(EventTenantRef(id="tenant-1"), event.tenant)
            self.assertEqual(EventObjectRef(type="mail_profile", id="profile-1"), event.resource)
            self.assertEqual("mail.profiles", event.payload["collection"])
            self.assertEqual({"scope_type": "tenant"}, event.payload["payload"])
        finally:
            engine.dispose()

    def test_record_change_discards_event_when_transaction_rolls_back(self) -> None:
        engine = create_engine("sqlite:///:memory:")
        SessionLocal = sessionmaker(bind=engine)
        try:
            Base.metadata.create_all(
                bind=engine,
                tables=[
                    ChangeSequenceEntry.__table__,
                    ChangeSequenceRetentionFloor.__table__,
                ],
            )
            seen: list[PlatformEvent] = []
            bus = EventBus()
            bus.subscribe("*", seen.append)
            with SessionLocal() as session, event_bus_context(bus):
                record_change(
                    session,
                    tenant_id="tenant-1",
                    module_id="files",
                    collection="files.assets",
                    resource_type="file",
                    resource_id="file-1",
                    operation="created",
                )
                session.rollback()

            self.assertEqual([], seen)
        finally:
            engine.dispose()

    def test_pruning_records_retention_floor_for_stale_watermarks(self) -> None:
        engine = create_engine("sqlite:///:memory:")
        SessionLocal = sessionmaker(bind=engine)
        try:
            Base.metadata.create_all(bind=engine, tables=[ChangeSequenceEntry.__table__, ChangeSequenceRetentionFloor.__table__])
            with SessionLocal() as session:
                first = record_change(
                    session,
                    tenant_id="tenant-1",
                    module_id="files",
                    collection="files.assets",
                    resource_type="file",
                    resource_id="file-1",
                    operation="created",
                )
                second = record_change(
                    session,
                    tenant_id="tenant-1",
                    module_id="files",
                    collection="files.assets",
                    resource_type="file",
                    resource_id="file-2",
                    operation="created",
                )
                session.commit()
                first_id = first.id
                second_resource_id = second.resource_id

                deleted = prune_sequence_entries(
                    session,
                    tenant_id="tenant-1",
                    module_id="files",
                    collections=("files.assets",),
                    before_or_equal_id=first_id,
                )
                session.commit()

                self.assertEqual(deleted, 1)
                self.assertEqual(retained_sequence_floor(session, tenant_id="tenant-1", module_id="files", collections=("files.assets",)), first_id)
                self.assertTrue(sequence_watermark_is_expired(session, tenant_id="tenant-1", module_id="files", collections=("files.assets",), since=0))
                self.assertFalse(sequence_watermark_is_expired(session, tenant_id="tenant-1", module_id="files", collections=("files.assets",), since=first_id))
                self.assertEqual(
                    [entry.resource_id for entry in sequence_entries_since(session, tenant_id="tenant-1", module_id="files", collections=("files.assets",), since=first_id)],
                    [second_resource_id],
                )
        finally:
            engine.dispose()

    def test_rejects_invalid_watermark(self) -> None:
        with self.assertRaises(ValueError):
            decode_sequence_watermark("seq:not-a-number")


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