"""Full integration test for the ECM DMS API (asynchronous) using the Unittest_DMS schema.

This test covers the complete lifecycle of ECM objects: create, read, update,
table field manipulation, cascading delete, recycle bin query, and hard delete.

Object hierarchy used in this test:
    folder (Unittest_DMS)
      register (Unittest_DMS_Register)
        subregister1 (Unittest_DMS_Subregister)  ← doc1 lives here
        subregister2 (Unittest_DMS_Subregister)  ← doc2 lives here

Note: Unittest_DMS_Document may only be placed inside Unittest_DMS_Subregister,
not directly inside Unittest_DMS_Register (schema constraint of the test cabinet).
"""

import os
import unittest
from datetime import date, datetime, time
from typing import Any
from uuid import UUID

from ecmind_blue_client.ecm import ECM
from ecmind_blue_client.ecm._ecm_exception import ECMAccessDeniedException, ECMNotFoundException, ECMWrongStateException
from ecmind_blue_client.pool import AsyncPoolClient
from ecmind_blue_client.rpc._blue_exception import BlueException
from tests.models.Unittest_DMS import (
    Unittest_DMS,
    Unittest_DMS_Document,
    Unittest_DMS_Document_TableFieldRow,
    Unittest_DMS_Register,
    Unittest_DMS_Subregister,
)
from tests._test_settings import ECM_SERVERS, ECM_USERNAME, ECM_PASSWORD


def _new_table_row(row_type: type, **fields: Any) -> Any:
    """Create a new ECMTableRowModel instance with the given field values.

    Since ECMTableRowModel does not define an ``__init__`` that accepts keyword
    arguments, we instantiate it via ``__new__`` and populate ``_raw_fields_``
    directly — the same pattern used internally by ``_ECMModelBase.__init__``
    when it converts dict items to row objects.

    Args:
        row_type: The row model class to instantiate.
        **fields: Column values keyed by ECM internal field name.

    Returns:
        A new row instance with the given field values and no server row ID.
    """
    row = object.__new__(row_type)
    row._row_id_ = None  # pylint: disable=protected-access
    row._raw_fields_ = dict(fields)  # pylint: disable=protected-access
    row._original_fields_ = {}  # pylint: disable=protected-access
    return row


class TestEcmDmsFullAsync(unittest.IsolatedAsyncioTestCase):
    """Full end-to-end async integration test for the ECM DMS object lifecycle."""

    def setUp(self):
        self.client = AsyncPoolClient(servers=ECM_SERVERS, username=ECM_USERNAME, password=ECM_PASSWORD)
        self.ecm = ECM(self.client)
        self.uuid = UUID(bytes=os.urandom(16), version=4)

    def tearDown(self):
        del self.client

    async def test_full_ecm_workflow(self):  # pylint: disable=too-many-statements
        """Complete async object lifecycle: CRUD, table manipulation, garbage bin, hard delete."""
        ecm = self.ecm
        uuid = self.uuid

        # ------------------------------------------------------------------ #
        # Steps 1-3: Insert folder, verify returned IDs, fetch by ID          #
        # ------------------------------------------------------------------ #
        folder_name = f"Unittest_{uuid}"
        folder_id, folder_type_id = await ecm.dms.insert(Unittest_DMS(Name=folder_name))

        self.assertGreater(folder_id, 0, "insert() must return a positive object ID")
        self.assertGreater(folder_type_id, 0, "insert() must return a positive object type ID")

        folder = await ecm.dms.get(Unittest_DMS, folder_id, folder_type_id)
        self.assertEqual(folder.Name, folder_name)

        # ------------------------------------------------------------------ #
        # Step 4: Rename folder and verify the change persists               #
        # ------------------------------------------------------------------ #
        updated_name = f"Unittest_{uuid}_updated"
        folder.Name = updated_name
        folder = await ecm.dms.update_and_get(folder)
        self.assertEqual(folder.Name, updated_name)

        # ------------------------------------------------------------------ #
        # Step 5: Inserting a folder with a missing mandatory field must fail #
        # ------------------------------------------------------------------ #
        with self.assertRaises(ValueError):
            await ecm.dms.insert(Unittest_DMS())  # type: ignore[call-arg]

        # ------------------------------------------------------------------ #
        # Step 6: Create a register inside the folder                         #
        # ------------------------------------------------------------------ #
        register: Unittest_DMS_Register = await ecm.dms.insert_and_get(
            Unittest_DMS_Register(Name=f"Register_{uuid}"),
            folder_id=folder_id,
        )
        self.assertIsNotNone(register.system.id)

        # ------------------------------------------------------------------ #
        # Step 7: Create subregister1 — home for doc1                         #
        # ------------------------------------------------------------------ #
        subregister1: Unittest_DMS_Subregister = await ecm.dms.insert_and_get(
            Unittest_DMS_Subregister(Name=f"Subregister1_{uuid}"),
            folder_id=folder_id,
            register_id=register.system.id,
        )
        self.assertIsNotNone(subregister1.system.id)

        # ------------------------------------------------------------------ #
        # Step 8: Create doc1 in subregister1, then set StringField = str(id) #
        # ------------------------------------------------------------------ #
        doc1: Unittest_DMS_Document = await ecm.dms.insert_and_get(
            Unittest_DMS_Document(Name=f"Document1_{uuid}"),
            folder_id=folder_id,
            register_id=subregister1.system.id,
        )
        doc1.StringField = str(doc1.system.id)
        doc1 = await ecm.dms.update_and_get(doc1)
        self.assertEqual(doc1.StringField, str(doc1.system.id))

        # ------------------------------------------------------------------ #
        # Step 9: Create subregister2 — home for doc2                         #
        # ------------------------------------------------------------------ #
        subregister2: Unittest_DMS_Subregister = await ecm.dms.insert_and_get(
            Unittest_DMS_Subregister(Name=f"Subregister2_{uuid}"),
            folder_id=folder_id,
            register_id=register.system.id,
        )
        self.assertIsNotNone(subregister2.system.id)

        # ------------------------------------------------------------------ #
        # Step 10: Insert doc2 in subregister2 with all scalar fields          #
        # ------------------------------------------------------------------ #
        test_date = date(2024, 6, 15)
        test_datetime = datetime(2024, 6, 15, 10, 30, 0)
        test_time = time(10, 30, 0)

        doc2: Unittest_DMS_Document = await ecm.dms.insert_and_get(
            Unittest_DMS_Document(
                Name=f"Document2_{uuid}",
                StringField="AllFieldsTest",
                DateField=test_date,
                DateTimeField=test_datetime,
                DoubleField=3.14,
                CheckboxField=True,
                RadioField1=1,
                OSID=9999,
                TimeField=test_time,
            ),
            folder_id=folder_id,
            register_id=subregister2.system.id,
        )

        # ------------------------------------------------------------------ #
        # Step 11: Reload doc2 and compare all scalar fields                   #
        # ------------------------------------------------------------------ #
        doc2 = await ecm.dms.get(Unittest_DMS_Document, doc2.system.id)
        self.assertEqual(doc2.Name, f"Document2_{uuid}")
        self.assertEqual(doc2.StringField, "AllFieldsTest")
        actual_date = doc2.DateField
        self.assertEqual(actual_date.date() if isinstance(actual_date, datetime) else actual_date, test_date)
        self.assertEqual(doc2.DateTimeField, test_datetime)
        self.assertIsNotNone(doc2.DoubleField)
        self.assertAlmostEqual(doc2.DoubleField, 3.14, places=5)  # type: ignore[arg-type]
        self.assertTrue(doc2.CheckboxField)
        self.assertEqual(doc2.RadioField1, 1)
        self.assertEqual(doc2.OSID, 9999)
        self.assertEqual(doc2.TimeField, test_time)

        # ------------------------------------------------------------------ #
        # Step 12: Append two table rows, save, reload, verify 2 rows          #
        # ------------------------------------------------------------------ #
        row1 = _new_table_row(
            Unittest_DMS_Document_TableFieldRow,
            TableCol1="Row1",
            TableCol2=100,
            TableCol3=date(2024, 1, 1),
            TableCol4=1.1,
        )
        row2 = _new_table_row(
            Unittest_DMS_Document_TableFieldRow,
            TableCol1="Row2",
            TableCol2=200,
            TableCol3=date(2024, 2, 1),
            TableCol4=2.2,
        )
        doc2.TableField.append(row1)
        doc2.TableField.append(row2)
        doc2 = await ecm.dms.update_and_get(doc2)
        self.assertEqual(len(doc2.TableField), 2)
        self.assertEqual(doc2.TableField[0].TableCol1, "Row1")
        self.assertEqual(doc2.TableField[1].TableCol1, "Row2")

        # ------------------------------------------------------------------ #
        # Step 13: Delete the first row and keep only Row2                    #
        # ------------------------------------------------------------------ #
        del doc2.TableField[0]
        doc2 = await ecm.dms.update_and_get(doc2)
        self.assertEqual(len(doc2.TableField), 1)
        self.assertEqual(doc2.TableField[0].TableCol1, "Row2")

        # ------------------------------------------------------------------ #
        # Step 14: Attempting to delete non-empty objects must fail           #
        # ------------------------------------------------------------------ #
        with self.assertRaises(ECMWrongStateException):
            await ecm.dms.delete(folder)

        with self.assertRaises(ECMWrongStateException):
            await ecm.dms.delete(register)

        with self.assertRaises(ECMWrongStateException):
            await ecm.dms.delete(subregister2)

        # ------------------------------------------------------------------ #
        # Step 15: Delete doc2                                                 #
        # ------------------------------------------------------------------ #
        await ecm.dms.delete(doc2)

        # ------------------------------------------------------------------ #
        # Step 16: Delete the now-empty subregister2                          #
        # ------------------------------------------------------------------ #
        await ecm.dms.delete(subregister2)

        # ------------------------------------------------------------------ #
        # Step 17: Delete the folder with cascading                           #
        # ------------------------------------------------------------------ #
        await ecm.dms.delete(folder, delete_cascading=True)

        # ------------------------------------------------------------------ #
        # Step 18: Query the recycle bin and verify the folder is there       #
        # ------------------------------------------------------------------ #
        deleted_folders = await ecm.dms.select(Unittest_DMS).garbage_mode(True).where(Unittest_DMS.Name == updated_name).execute()
        self.assertEqual(len(deleted_folders), 1, "Soft-deleted folder must appear in garbage query")
        self.assertIsNotNone(deleted_folders[0].system.deleted_at, "deleted_at must be set for a soft-deleted object")

        folder = deleted_folders[0]

        # ------------------------------------------------------------------ #
        # Step 19: Permanently remove the folder from the recycle bin         #
        # ------------------------------------------------------------------ #
        await ecm.dms.delete(folder, hard_delete=True)

        # ------------------------------------------------------------------ #
        # Step 20: After a hard delete the object must no longer be findable  #
        # ------------------------------------------------------------------ #
        with self.assertRaises((ECMNotFoundException, ECMAccessDeniedException, BlueException)):
            await ecm.dms.get(Unittest_DMS, folder_id)


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