"""Integration tests for concurrency_timestamp in ECM update operations.

Verifies that:
- Normal updates with a valid concurrency_timestamp succeed.
- The server rejects updates whose concurrency_timestamp is stale (ECMWrongStateException,
  underlying server error -1048 "A concurrent update of an object failed").

The concurrency_timestamp mechanism works as follows:
  1. The server stores MODIFYTIME (last modification time) for every object.
  2. When the client reads an object, OBJECT_MODIFYTIME is populated in _raw_fields_.
  3. On update, _build_update_xml() converts that datetime to a Unix long and sets
     concurrency_timestamp on the <Object> element.
  4. The server compares that value against its stored MODIFYTIME.
     - Match  → update proceeds (return code 0).
     - Mismatch → update rejected (server return code -1048), raised as ECMWrongStateException.
  5. Setting concurrency_timestamp=-1 (default when OBJECT_MODIFYTIME is absent) disables
     the check entirely.
"""

import os
import time
import unittest
from datetime import datetime
from uuid import UUID

from ecmind_blue_client.ecm import ECM
from ecmind_blue_client.ecm._ecm_exception import ECMWrongStateException
from ecmind_blue_client.pool import SyncPoolClient
from tests.models.Unittest_DMS import Unittest_DMS
from tests._test_settings import ECM_SERVERS, ECM_USERNAME, ECM_PASSWORD


class TestConcurrencyTimestampIntegration(unittest.TestCase):
    """Integration tests for concurrency_timestamp functionality."""

    def setUp(self):
        self.client = SyncPoolClient(servers=ECM_SERVERS, username=ECM_USERNAME, password=ECM_PASSWORD)
        self.ecm = ECM(self.client)
        self.uuid = UUID(bytes=os.urandom(16), version=4)
        self._cleanup_ids: list[int] = []

    def tearDown(self):
        """Delete any objects that were created during the test."""
        for obj_id in self._cleanup_ids:
            try:
                obj = self.ecm.dms.get(Unittest_DMS, obj_id)
                self.ecm.dms.delete(obj)
            except Exception:  # pylint: disable=broad-except
                pass
        del self.client

    def _insert_folder(self, name: str) -> Unittest_DMS:
        """Insert a folder, register it for cleanup, and return the fetched instance."""
        folder_id, folder_type_id = self.ecm.dms.insert(Unittest_DMS(Name=name))
        self._cleanup_ids.append(folder_id)
        return self.ecm.dms.get(Unittest_DMS, folder_id, folder_type_id)

    # ------------------------------------------------------------------
    # Success path: valid timestamp
    # ------------------------------------------------------------------

    def test_update_with_valid_concurrency_timestamp_succeeds(self):
        """Normal update with the correct concurrency_timestamp must succeed."""
        folder = self._insert_folder(f"ConcurrencyValid_{self.uuid}")

        self.assertIsNotNone(folder.system.last_modified, "Folder must have a last_modified after get()")

        folder.Name = f"ConcurrencyValid_{self.uuid}_updated"
        updated = self.ecm.dms.update_and_get(folder)

        self.assertEqual(updated.Name, f"ConcurrencyValid_{self.uuid}_updated")
        self.assertIsNotNone(updated.system.last_modified)

    # ------------------------------------------------------------------
    # Error path: stale timestamp → server returns -1048 → ECMWrongStateException
    # ------------------------------------------------------------------

    def test_update_with_stale_concurrency_timestamp_raises_wrong_state(self):
        """The server must reject an update whose concurrency_timestamp is stale.

        Simulates a concurrent modification by overwriting OBJECT_MODIFYTIME in
        _raw_fields_ with a datetime far in the past. The server compares this against
        its stored MODIFYTIME, detects a mismatch, and returns error -1048
        ('A concurrent update of an object failed'), which the client raises as
        ECMWrongStateException.
        """
        folder = self._insert_folder(f"ConcurrencyStale_{self.uuid}")

        # Force an obviously wrong modification time to simulate a stale read.
        folder._raw_fields_["OBJECT_MODIFYTIME"] = datetime(2000, 1, 1)  # pylint: disable=protected-access
        folder.Name = f"ConcurrencyStale_{self.uuid}_should_fail"

        with self.assertRaises(ECMWrongStateException):
            self.ecm.dms.update_and_get(folder)

    def test_update_without_concurrency_timestamp_always_succeeds(self):
        """Update without OBJECT_MODIFYTIME must succeed unconditionally.

        When OBJECT_MODIFYTIME is absent, _build_update_xml() omits concurrency_timestamp
        (equivalent to -1), so the server skips the check and applies the update.
        """
        folder = self._insert_folder(f"ConcurrencyNone_{self.uuid}")

        folder._raw_fields_.pop("OBJECT_MODIFYTIME", None)  # pylint: disable=protected-access
        folder.Name = f"ConcurrencyNone_{self.uuid}_updated"

        updated = self.ecm.dms.update_and_get(folder)
        self.assertEqual(updated.Name, f"ConcurrencyNone_{self.uuid}_updated")

    def test_second_update_with_original_timestamp_is_rejected(self):
        """Simulates a real concurrency conflict: two clients reading the same object.

        Client A and client B both read the same folder (same OBJECT_MODIFYTIME).
        Client A updates first — the server advances MODIFYTIME.
        Client B then tries to update using the now-stale original timestamp and must
        receive ECMWrongStateException (server error -1048).
        """
        folder_a = self._insert_folder(f"ConcurrencyConflict_{self.uuid}")
        folder_b = self.ecm.dms.get(Unittest_DMS, folder_a.system.id)

        # Both clients now hold OBJECT_MODIFYTIME = T_insert. Sleep 1 s before
        # client A's update so that the server writes a MODIFYTIME in a strictly
        # later second (MODIFYTIME has second granularity). Without the sleep,
        # T_update could equal T_insert and client B's stale timestamp would
        # accidentally match — no conflict would be detected.
        time.sleep(1)

        folder_a.Name = f"ConcurrencyConflict_{self.uuid}_clientA"
        self.ecm.dms.update_and_get(folder_a)

        # Client B still holds the old MODIFYTIME (T_insert < T_update) → must be rejected.
        folder_b.Name = f"ConcurrencyConflict_{self.uuid}_clientB"
        with self.assertRaises(ECMWrongStateException):
            self.ecm.dms.update_and_get(folder_b)


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