"""Integration tests for HOL queries with children and parent objects.

Requires a running ECM server at localhost:4000 with the Unittest_DMS cabinet.

Object hierarchy used throughout this test module:

    folder (Unittest_DMS)
      register (Unittest_DMS_Register)
        subregister (Unittest_DMS_Subregister)
          document (Unittest_DMS_Document)

Tests cover:
- ``with_children()``: typed and untyped specs, direct-only collection.
- ``with_parents()``: outermost parent as ``result.main``, all intermediate
  ancestor types accessible via ``result.children_of()``.
- Limit enforcement: ``stream()`` / ``execute()`` without ``limit()`` must raise
  ``ValueError``.
"""

import os
import unittest
from uuid import UUID

from ecmind_blue_client.ecm import ECM
from ecmind_blue_client.ecm.model._ecm_hol_spec import ECMChildSpec, ECMParentSpec
from ecmind_blue_client.ecm.model._ecm_model import _ECMModelBase
from ecmind_blue_client.pool import SyncPoolClient
from tests.models.Unittest_DMS import (
    Unittest_DMS,
    Unittest_DMS_Document,
    Unittest_DMS_Register,
    Unittest_DMS_Subregister,
)
from tests._test_settings import ECM_SERVERS, ECM_USERNAME, ECM_PASSWORD


class TestEcmHol(unittest.TestCase):
    """Integration tests for HOL child/parent query 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._folder_ids: list[int] = []

    def tearDown(self):
        """Remove all folders created during the test (cascading delete)."""
        for folder_id in self._folder_ids:
            try:
                folder = self.ecm.dms.get(Unittest_DMS, folder_id)
                self.ecm.dms.delete(folder, delete_cascading=True)
            except Exception:  # pylint: disable=broad-except
                pass
        del self.client

    # ------------------------------------------------------------------
    # Setup helpers
    # ------------------------------------------------------------------

    def _insert_folder(self, suffix: str = "") -> Unittest_DMS:
        """Insert a folder and register its ID for cleanup."""
        name = f"HOL_{self.uuid}{suffix}"
        folder_id, folder_type_id = self.ecm.dms.insert(Unittest_DMS(Name=name))
        self._folder_ids.append(folder_id)
        return self.ecm.dms.get(Unittest_DMS, folder_id, folder_type_id)

    def _insert_register(self, folder: Unittest_DMS, suffix: str = "") -> Unittest_DMS_Register:
        return self.ecm.dms.insert_and_get(
            Unittest_DMS_Register(Name=f"Reg_{self.uuid}{suffix}"),
            folder_id=folder.system.id,
        )

    def _insert_subregister(self, folder: Unittest_DMS, register: Unittest_DMS_Register, suffix: str = "") -> Unittest_DMS_Subregister:
        return self.ecm.dms.insert_and_get(
            Unittest_DMS_Subregister(Name=f"Sub_{self.uuid}{suffix}"),
            folder_id=folder.system.id,
            register_id=register.system.id,
        )

    def _insert_document(
        self,
        folder: Unittest_DMS,
        subregister: Unittest_DMS_Subregister,
        suffix: str = "",
    ) -> Unittest_DMS_Document:
        return self.ecm.dms.insert_and_get(
            Unittest_DMS_Document(Name=f"Doc_{self.uuid}{suffix}"),
            folder_id=folder.system.id,
            register_id=subregister.system.id,
        )

    # ------------------------------------------------------------------
    # Limit enforcement (no server round-trip needed)
    # ------------------------------------------------------------------

    def test_stream_without_limit_raises_value_error(self):
        """``stream()`` raises ``ValueError`` when ``limit()`` was not called."""
        base_query = self.ecm.dms.select(Unittest_DMS)
        hol_query = base_query.with_children(ECMChildSpec(Unittest_DMS_Register))
        with self.assertRaises(ValueError):
            list(hol_query.stream())

    def test_with_parents_also_requires_limit(self):
        """The limit requirement applies to ``with_parents()`` queries too."""
        base_query = self.ecm.dms.select(Unittest_DMS_Document)
        hol_query = base_query.with_parents(ECMParentSpec(Unittest_DMS))
        with self.assertRaises(ValueError):
            hol_query.execute()

    # ------------------------------------------------------------------
    # with_children() — typed spec
    # ------------------------------------------------------------------

    def test_with_children_folder_gets_registers(self):
        """Query a folder with typed Register child spec returns the correct children."""
        folder = self._insert_folder()
        reg1 = self._insert_register(folder, "_A")
        reg2 = self._insert_register(folder, "_B")

        results = (
            self.ecm.dms.select(Unittest_DMS)
            .where(Unittest_DMS.Name == folder.Name)
            .with_children(ECMChildSpec(Unittest_DMS_Register))
            .limit(10)
            .execute()
        )

        self.assertEqual(len(results), 1)
        main = results[0].main
        self.assertIsInstance(main, Unittest_DMS)
        self.assertEqual(main.Name, folder.Name)

        registers = results[0].children_of(ECMChildSpec(Unittest_DMS_Register))
        register_ids = {r.system.id for r in registers}
        self.assertIn(reg1.system.id, register_ids)
        self.assertIn(reg2.system.id, register_ids)

    def test_with_children_register_gets_subregisters(self):
        """Query a register with typed Subregister child spec returns the correct children."""
        folder = self._insert_folder()
        register = self._insert_register(folder)
        sub1 = self._insert_subregister(folder, register, "_A")
        sub2 = self._insert_subregister(folder, register, "_B")

        results = (
            self.ecm.dms.select(Unittest_DMS_Register)
            .where(Unittest_DMS_Register.Name == register.Name)
            .with_children(ECMChildSpec(Unittest_DMS_Subregister))
            .limit(10)
            .execute()
        )

        self.assertEqual(len(results), 1)
        self.assertIsInstance(results[0].main, Unittest_DMS_Register)

        subs = results[0].children_of(ECMChildSpec(Unittest_DMS_Subregister))
        sub_ids = {s.system.id for s in subs}
        self.assertIn(sub1.system.id, sub_ids)
        self.assertIn(sub2.system.id, sub_ids)

    def test_with_children_no_specs_returns_all_child_types_generically(self):
        """``with_children()`` without specs returns all child types as generic models."""
        folder = self._insert_folder()
        self._insert_register(folder)

        results = self.ecm.dms.select(Unittest_DMS).where(Unittest_DMS.Name == folder.Name).with_children().limit(10).execute()

        self.assertEqual(len(results), 1)
        # At least the register must appear; type is generic _ECMModelBase.
        by_internal_name = results[0].children_of("Unittest_DMS_Register")
        self.assertGreater(len(by_internal_name), 0)
        self.assertIsInstance(by_internal_name[0], _ECMModelBase)

    def test_with_children_empty_folder_returns_empty_list(self):
        """A folder with no children returns an empty ``children_of()`` list."""
        folder = self._insert_folder()

        results = (
            self.ecm.dms.select(Unittest_DMS)
            .where(Unittest_DMS.Name == folder.Name)
            .with_children(ECMChildSpec(Unittest_DMS_Register))
            .limit(10)
            .execute()
        )

        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].children_of(ECMChildSpec(Unittest_DMS_Register)), [])

    def test_with_children_direct_only_does_not_include_subregisters(self):
        """``with_children(Register)`` on a folder returns only direct register children.

        Even when a subregister exists deeper in the hierarchy, it must NOT appear
        in the direct-children result because ``_collect_children`` is not recursive.
        """
        folder = self._insert_folder()
        register = self._insert_register(folder)
        self._insert_subregister(folder, register)

        results = (
            self.ecm.dms.select(Unittest_DMS)
            .where(Unittest_DMS.Name == folder.Name)
            .with_children(ECMChildSpec(Unittest_DMS_Register))
            .limit(10)
            .execute()
        )

        self.assertEqual(len(results), 1)
        # Direct children only: the register — subregister must NOT appear.
        registers = results[0].children_of(ECMChildSpec(Unittest_DMS_Register))
        self.assertEqual(len(registers), 1)
        self.assertEqual(registers[0].system.id, register.system.id)

        subregisters = results[0].children_of(ECMChildSpec(Unittest_DMS_Subregister))
        self.assertEqual(subregisters, [], "Subregisters must not appear in direct-children result")

    # ------------------------------------------------------------------
    # with_parents()
    # ------------------------------------------------------------------

    def test_with_parents_document_main_is_folder(self):
        """Querying documents with parents returns the folder as ``result.main``."""
        folder = self._insert_folder()
        register = self._insert_register(folder)
        subregister = self._insert_subregister(folder, register)
        document = self._insert_document(folder, subregister)

        results = (
            self.ecm.dms.select(Unittest_DMS_Document)
            .where(Unittest_DMS_Document.Name == document.Name)
            .with_parents(
                ECMParentSpec(Unittest_DMS),
                ECMParentSpec(Unittest_DMS_Register),
                ECMParentSpec(Unittest_DMS_Subregister),
            )
            .limit(10)
            .execute()
        )

        self.assertGreater(len(results), 0)
        # The outermost parent (Folder) is main.
        main = results[0].main
        self.assertIsInstance(main, Unittest_DMS)
        self.assertEqual(main.system.id, folder.system.id)

    def test_with_parents_document_accessible_as_descendant(self):
        """The queried document is accessible via ``children_of(ECMChildSpec(Document))``."""
        folder = self._insert_folder()
        register = self._insert_register(folder)
        subregister = self._insert_subregister(folder, register)
        document = self._insert_document(folder, subregister)

        results = (
            self.ecm.dms.select(Unittest_DMS_Document)
            .where(Unittest_DMS_Document.Name == document.Name)
            .with_parents(
                ECMParentSpec(Unittest_DMS),
                ECMParentSpec(Unittest_DMS_Register),
                ECMParentSpec(Unittest_DMS_Subregister),
            )
            .limit(10)
            .execute()
        )

        self.assertGreater(len(results), 0)
        docs = results[0].children_of(ECMChildSpec(Unittest_DMS_Document))
        doc_ids = {d.system.id for d in docs}
        self.assertIn(document.system.id, doc_ids)

    def test_with_parents_intermediate_types_accessible(self):
        """Intermediate parent types listed in specs are accessible via ``children_of()``.

        For ``with_parents(Folder, Register, Subregister)``:
        - ``result.main``                                  → Folder
        - ``result.children_of(ECMChildSpec(Register))``  → Register
        - ``result.children_of(ECMChildSpec(Subregister))`` → Subregister
        - ``result.children_of(ECMChildSpec(Document))``  → Document
        """
        folder = self._insert_folder()
        register = self._insert_register(folder)
        subregister = self._insert_subregister(folder, register)
        document = self._insert_document(folder, subregister)

        results = (
            self.ecm.dms.select(Unittest_DMS_Document)
            .where(Unittest_DMS_Document.Name == document.Name)
            .with_parents(
                ECMParentSpec(Unittest_DMS),
                ECMParentSpec(Unittest_DMS_Register),
                ECMParentSpec(Unittest_DMS_Subregister),
            )
            .limit(10)
            .execute()
        )

        self.assertGreater(len(results), 0)
        result = results[0]

        registers = result.children_of(ECMChildSpec(Unittest_DMS_Register))
        self.assertGreater(len(registers), 0)
        reg_ids = {r.system.id for r in registers}
        self.assertIn(register.system.id, reg_ids)

        subregisters = result.children_of(ECMChildSpec(Unittest_DMS_Subregister))
        self.assertGreater(len(subregisters), 0)
        sub_ids = {s.system.id for s in subregisters}
        self.assertIn(subregister.system.id, sub_ids)

    def test_with_parents_full_hierarchy_collected(self):
        """All four levels of the hierarchy are accessible from one HOL query.

        Structure: Folder → Register → Subregister → Document

        After ``with_parents(Folder, Register, Subregister)`` on Document:
        - ``result.main``          → Folder (outermost)
        - ``children_of(Register)``    → [Register]
        - ``children_of(Subregister)`` → [Subregister]
        - ``children_of(Document)``    → [Document]

        This is the key end-to-end test for the multi-level parent traversal.
        """
        folder = self._insert_folder()
        register = self._insert_register(folder)
        subregister = self._insert_subregister(folder, register)
        document = self._insert_document(folder, subregister)

        results = (
            self.ecm.dms.select(Unittest_DMS_Document)
            .where(Unittest_DMS_Document.Name == document.Name)
            .with_parents(
                ECMParentSpec(Unittest_DMS),
                ECMParentSpec(Unittest_DMS_Register),
                ECMParentSpec(Unittest_DMS_Subregister),
            )
            .limit(10)
            .execute()
        )

        self.assertGreater(len(results), 0, "At least one result must be returned")
        result = results[0]

        self.assertIsInstance(result.main, Unittest_DMS)
        self.assertEqual(result.main.system.id, folder.system.id)

        self.assertGreater(len(result.children_of(ECMChildSpec(Unittest_DMS_Register))), 0)
        self.assertGreater(len(result.children_of(ECMChildSpec(Unittest_DMS_Subregister))), 0)
        self.assertGreater(len(result.children_of(ECMChildSpec(Unittest_DMS_Document))), 0)

        self.assertEqual(
            result.children_of(ECMChildSpec(Unittest_DMS_Document))[0].system.id,
            document.system.id,
        )

    def test_with_parents_multiple_documents_multiple_results(self):
        """Multiple documents with the same parent produce multiple HOL results."""
        folder = self._insert_folder()
        register = self._insert_register(folder)
        subregister = self._insert_subregister(folder, register)
        doc1 = self._insert_document(folder, subregister, "_A")
        doc2 = self._insert_document(folder, subregister, "_B")

        results = (
            self.ecm.dms.select(Unittest_DMS_Document)
            .where((Unittest_DMS_Document.Name == doc1.Name) | (Unittest_DMS_Document.Name == doc2.Name))
            .with_parents(ECMParentSpec(Unittest_DMS))
            .limit(10)
            .execute()
        )

        # Each document produces its own result row (server returns one folder per doc).
        found_doc_ids = set()
        for r in results:
            self.assertIsInstance(r.main, Unittest_DMS)
            for d in r.children_of(ECMChildSpec(Unittest_DMS_Document)):
                found_doc_ids.add(d.system.id)

        self.assertIn(doc1.system.id, found_doc_ids)
        self.assertIn(doc2.system.id, found_doc_ids)

    def test_with_parents_lookup_by_string_internal_name(self):
        """Descendants can be looked up by plain string internal_name."""
        folder = self._insert_folder()
        register = self._insert_register(folder)
        subregister = self._insert_subregister(folder, register)
        document = self._insert_document(folder, subregister)

        results = (
            self.ecm.dms.select(Unittest_DMS_Document)
            .where(Unittest_DMS_Document.Name == document.Name)
            .with_parents(ECMParentSpec(Unittest_DMS))
            .limit(10)
            .execute()
        )

        self.assertGreater(len(results), 0)
        docs = results[0].children_of("Unittest_DMS_Document")
        self.assertGreater(len(docs), 0)


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