from __future__ import annotations

import unittest

from sqlalchemy import Integer, String, create_engine
from sqlalchemy.orm import Mapped, Session, mapped_column, sessionmaker
from sqlalchemy.pool import StaticPool

from govoplan_core.core.concurrency import (
    MissingPreconditionError,
    RevisionConflictError,
    assert_revision_precondition,
    claim_revision,
    if_match_matches,
    strong_resource_etag,
    three_way_merge,
)
from govoplan_core.db.base import Base


class _RevisionFixture(Base):
    __tablename__ = "test_revision_fixture"

    id: Mapped[str] = mapped_column(String(36), primary_key=True)
    edit_revision: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
    value: Mapped[str] = mapped_column(String(100), nullable=False)


class OptimisticConcurrencyTests(unittest.TestCase):
    def test_strong_etags_and_if_match_use_strong_comparison(self) -> None:
        etag = strong_resource_etag("campaign_version", "version-1", 3)

        self.assertTrue(if_match_matches(etag, etag))
        self.assertTrue(if_match_matches(f'"other", {etag}', etag))
        self.assertFalse(if_match_matches(f"W/{etag}", etag))
        self.assertNotEqual(
            etag,
            strong_resource_etag("campaign_version", "version-1", 4),
        )
        with self.assertRaises(MissingPreconditionError):
            assert_revision_precondition(
                None,
                resource_type="campaign_version",
                resource_id="version-1",
                submitted_base_revision=3,
            )

    def test_compare_and_set_allows_only_one_writer_for_a_revision(self) -> None:
        engine = create_engine(
            "sqlite+pysqlite://",
            connect_args={"check_same_thread": False},
            poolclass=StaticPool,
        )
        _RevisionFixture.__table__.create(engine)
        session_factory = sessionmaker(bind=engine, class_=Session)
        with session_factory() as session:
            session.add(_RevisionFixture(id="resource-1", value="base"))
            session.commit()
            next_revision = claim_revision(
                session,
                model=_RevisionFixture,
                filters=(_RevisionFixture.id == "resource-1",),
                revision_attribute="edit_revision",
                expected_revision=1,
                resource_type="fixture",
                resource_id="resource-1",
            )
            session.commit()
            self.assertEqual(next_revision, 2)

            with self.assertRaises(RevisionConflictError) as raised:
                claim_revision(
                    session,
                    model=_RevisionFixture,
                    filters=(_RevisionFixture.id == "resource-1",),
                    revision_attribute="edit_revision",
                    expected_revision=1,
                    resource_type="fixture",
                    resource_id="resource-1",
                )
            self.assertEqual(raised.exception.current_revision, 2)
        engine.dispose()

    def test_disjoint_map_and_stable_id_collection_changes_merge(self) -> None:
        base = {
            "campaign": {"name": "Original", "description": "Base"},
            "entries": {
                "inline": [
                    {"id": "one", "name": "One", "active": True},
                    {"id": "two", "name": "Two", "active": True},
                ]
            },
        }
        local = {
            **base,
            "campaign": {"name": "Local", "description": "Base"},
            "entries": {
                "inline": [
                    {"id": "one", "name": "One", "active": False},
                    {"id": "two", "name": "Two", "active": True},
                ]
            },
        }
        current = {
            **base,
            "campaign": {"name": "Original", "description": "Remote"},
            "entries": {
                "inline": [
                    {"id": "one", "name": "One", "active": True},
                    {"id": "two", "name": "Remote Two", "active": True},
                ]
            },
        }

        result = three_way_merge(base, local, current)

        self.assertTrue(result.merged)
        self.assertEqual(result.value["campaign"], {"name": "Local", "description": "Remote"})
        self.assertEqual(result.value["entries"]["inline"][0]["active"], False)
        self.assertEqual(result.value["entries"]["inline"][1]["name"], "Remote Two")

    def test_same_path_unkeyed_collection_and_protected_edits_conflict(self) -> None:
        same_path = three_way_merge(
            {"name": "Base"},
            {"name": "Local"},
            {"name": "Remote"},
        )
        self.assertEqual(same_path.conflicts[0].kind, "same_path_changed")

        unkeyed = three_way_merge(
            {"values": ["a"]},
            {"values": ["a", "local"]},
            {"values": ["a", "remote"]},
        )
        self.assertEqual(unkeyed.conflicts[0].kind, "unkeyed_collection")

        protected = three_way_merge(
            {"workflow_state": "draft", "name": "Base"},
            {"workflow_state": "ready", "name": "Base"},
            {"workflow_state": "draft", "name": "Remote"},
            protected_paths=("/workflow_state",),
        )
        self.assertEqual(protected.conflicts[0].kind, "protected_path")

    def test_delete_versus_edit_and_conflicting_reorder_remain_explicit(self) -> None:
        base = [{"id": "one", "value": "1"}, {"id": "two", "value": "2"}]
        delete_vs_edit = three_way_merge(
            base,
            [{"id": "two", "value": "2"}],
            [{"id": "one", "value": "remote"}, {"id": "two", "value": "2"}],
        )
        self.assertEqual(delete_vs_edit.conflicts[0].kind, "delete_vs_edit")

        reordered = three_way_merge(
            [
                {"id": "one"},
                {"id": "two"},
                {"id": "three"},
            ],
            [
                {"id": "two"},
                {"id": "one"},
                {"id": "three"},
            ],
            [
                {"id": "one"},
                {"id": "three"},
                {"id": "two"},
            ],
        )
        self.assertEqual(reordered.conflicts[0].kind, "collection_reorder")


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