"""
Integration tests for SpaceService.

Tests the full workflow of space operations including:
- Space creation and lifecycle management
- Document operations within spaces
- Variable management
- Reference tracking for cache invalidation
"""

import pytest
import tempfile
import os

from markitect.spaces import (
    SpaceService,
    InformationSpace,
    SpaceDocument,
    SpaceConfig,
    SpaceMetadata,
    SpaceStatus,
    SqliteSpaceRepository,
    SqliteDocumentRepository,
    SqliteVariableRepository,
    SqliteReferenceRepository,
)


@pytest.fixture
def temp_db():
    """Create a temporary database file for testing."""
    fd, path = tempfile.mkstemp(suffix=".db")
    os.close(fd)
    yield path
    if os.path.exists(path):
        os.unlink(path)


@pytest.fixture
def space_service(temp_db):
    """Create a fully wired SpaceService for testing."""
    return SpaceService(
        space_repo=SqliteSpaceRepository(temp_db),
        document_repo=SqliteDocumentRepository(temp_db),
        variable_repo=SqliteVariableRepository(temp_db),
        reference_repo=SqliteReferenceRepository(temp_db),
    )


class TestSpaceLifecycle:
    """Tests for space lifecycle operations."""

    def test_create_and_retrieve_space(self, space_service):
        """Test creating and retrieving a space."""
        space = space_service.create_space(
            name="my-docs",
            description="My documentation",
        )

        assert space.name == "my-docs"
        assert space.description == "My documentation"
        assert space.status == SpaceStatus.DRAFT

        # Retrieve by ID
        retrieved = space_service.get_space(space.id)
        assert retrieved is not None
        assert retrieved.name == "my-docs"

        # Retrieve by name
        by_name = space_service.get_space_by_name("my-docs")
        assert by_name is not None
        assert by_name.id == space.id

    def test_create_space_with_config_and_metadata(self, space_service):
        """Test creating a space with custom config and metadata."""
        config = SpaceConfig(
            theme="dark",
            history_enabled=True,
            enable_caching=False,
        )
        metadata = SpaceMetadata(
            tags=["api", "v1"],
            author="tester",
            custom={"version": "1.0"},
        )

        space = space_service.create_space(
            name="configured-space",
            config=config,
            metadata=metadata,
        )

        assert space.config.theme == "dark"
        assert space.config.history_enabled is True
        assert space.metadata.tags == ["api", "v1"]
        assert space.metadata.author == "tester"

    def test_update_space(self, space_service):
        """Test updating a space."""
        space = space_service.create_space(name="original")

        updated = space_service.update_space(
            space.id,
            name="updated",
            description="New description",
        )

        assert updated.name == "updated"
        assert updated.description == "New description"

        # Verify persisted
        retrieved = space_service.get_space(space.id)
        assert retrieved.name == "updated"

    def test_space_lifecycle_transitions(self, space_service):
        """Test space status transitions."""
        space = space_service.create_space(name="lifecycle-test")
        assert space.status == SpaceStatus.DRAFT

        # Activate
        activated = space_service.activate_space(space.id)
        assert activated.status == SpaceStatus.ACTIVE

        # Archive
        archived = space_service.archive_space(space.id)
        assert archived.status == SpaceStatus.ARCHIVED

    def test_delete_space(self, space_service):
        """Test deleting a space."""
        space = space_service.create_space(name="to-delete")

        result = space_service.delete_space(space.id)
        assert result is True

        # Verify deleted
        retrieved = space_service.get_space(space.id)
        assert retrieved is None

    def test_list_spaces_excludes_archived(self, space_service):
        """Test that list_spaces excludes archived by default."""
        space1 = space_service.create_space(name="active")
        space2 = space_service.create_space(name="archived")
        space_service.archive_space(space2.id)

        spaces = space_service.list_spaces()
        assert len(spaces) == 1
        assert spaces[0].name == "active"

        # Include archived
        all_spaces = space_service.list_spaces(include_archived=True)
        assert len(all_spaces) == 2


class TestSpaceHierarchy:
    """Tests for space hierarchy operations."""

    def test_create_child_space(self, space_service):
        """Test creating a child space."""
        parent = space_service.create_space(name="parent")
        child = space_service.create_space(
            name="child",
            parent_space_id=parent.id,
        )

        assert child.parent_space_id == parent.id

        children = space_service.get_child_spaces(parent.id)
        assert len(children) == 1
        assert children[0].id == child.id

    def test_create_nested_hierarchy(self, space_service):
        """Test creating a nested space hierarchy."""
        root = space_service.create_space(name="root")
        level1 = space_service.create_space(name="level1", parent_space_id=root.id)
        level2 = space_service.create_space(name="level2", parent_space_id=level1.id)

        # Verify hierarchy
        root_children = space_service.get_child_spaces(root.id)
        assert len(root_children) == 1
        assert root_children[0].id == level1.id

        level1_children = space_service.get_child_spaces(level1.id)
        assert len(level1_children) == 1
        assert level1_children[0].id == level2.id

    def test_delete_space_with_children_cascade(self, space_service):
        """Test deleting a space cascades to children."""
        parent = space_service.create_space(name="parent")
        child = space_service.create_space(name="child", parent_space_id=parent.id)

        space_service.delete_space(parent.id, cascade=True)

        assert space_service.get_space(parent.id) is None
        assert space_service.get_space(child.id) is None

    def test_delete_space_with_children_no_cascade_raises(self, space_service):
        """Test deleting a space with children raises if cascade=False."""
        parent = space_service.create_space(name="parent")
        space_service.create_space(name="child", parent_space_id=parent.id)

        with pytest.raises(ValueError, match="has 1 child"):
            space_service.delete_space(parent.id, cascade=False)


class TestDocumentOperations:
    """Tests for document operations within spaces."""

    def test_add_and_list_documents(self, space_service):
        """Test adding and listing documents."""
        space = space_service.create_space(name="doc-space")

        doc1 = space_service.add_document(
            space.id,
            space_path="/intro.md",
            document_id="doc-1",
        )
        doc2 = space_service.add_document(
            space.id,
            space_path="/api/endpoints.md",
            document_id="doc-2",
        )

        docs = space_service.list_documents(space.id)
        assert len(docs) == 2

    def test_get_document_by_path(self, space_service):
        """Test getting a document by its path."""
        space = space_service.create_space(name="doc-space")
        space_service.add_document(space.id, "/intro.md", document_id="doc-1")

        doc = space_service.get_document_by_path(space.id, "/intro.md")
        assert doc is not None
        assert doc.document_id == "doc-1"

        # Also works without leading slash
        doc2 = space_service.get_document_by_path(space.id, "intro.md")
        assert doc2 is not None

    def test_move_document(self, space_service):
        """Test moving a document to a new path."""
        space = space_service.create_space(name="doc-space")
        doc = space_service.add_document(space.id, "/old-path.md")

        moved = space_service.move_document(doc.id, "/new-path.md")
        assert moved.space_path == "/new-path.md"

        # Old path should not exist
        old_doc = space_service.get_document_by_path(space.id, "/old-path.md")
        assert old_doc is None

        # New path should work
        new_doc = space_service.get_document_by_path(space.id, "/new-path.md")
        assert new_doc is not None

    def test_remove_document(self, space_service):
        """Test removing a document."""
        space = space_service.create_space(name="doc-space")
        doc = space_service.add_document(space.id, "/to-remove.md")

        result = space_service.remove_document(doc.id)
        assert result is True

        # Verify removed
        retrieved = space_service.get_document(doc.id)
        assert retrieved is None

    def test_reorder_documents(self, space_service):
        """Test reordering documents."""
        space = space_service.create_space(name="doc-space")
        doc1 = space_service.add_document(space.id, "/a.md", order_index=0)
        doc2 = space_service.add_document(space.id, "/b.md", order_index=1)
        doc3 = space_service.add_document(space.id, "/c.md", order_index=2)

        # Reorder: c, a, b
        space_service.reorder_documents(space.id, [doc3.id, doc1.id, doc2.id])

        docs = space_service.list_documents(space.id)
        assert docs[0].id == doc3.id
        assert docs[1].id == doc1.id
        assert docs[2].id == doc2.id

    def test_document_with_metadata(self, space_service):
        """Test document with custom metadata."""
        space = space_service.create_space(name="doc-space")
        doc = space_service.add_document(
            space.id,
            "/api.md",
            metadata={"title": "API Reference", "order": 5},
        )

        retrieved = space_service.get_document(doc.id)
        assert retrieved.metadata["title"] == "API Reference"
        assert retrieved.metadata["order"] == 5

    def test_update_document_hash(self, space_service):
        """Test updating document content hash."""
        space = space_service.create_space(name="doc-space")
        doc = space_service.add_document(space.id, "/content.md")

        space_service.update_document_hash(doc.id, "hash123abc")

        retrieved = space_service.get_document(doc.id)
        assert retrieved.content_hash == "hash123abc"


class TestVariableOperations:
    """Tests for variable operations within spaces."""

    def test_set_and_get_variable(self, space_service):
        """Test setting and getting a variable."""
        space = space_service.create_space(name="var-space")

        var = space_service.set_variable(space.id, "version", "1.0.0")
        assert var.value == "1.0.0"

        retrieved = space_service.get_variable(space.id, "version")
        assert retrieved is not None
        assert retrieved.value == "1.0.0"

    def test_list_variables(self, space_service):
        """Test listing variables."""
        space = space_service.create_space(name="var-space")
        space_service.set_variable(space.id, "var1", "value1")
        space_service.set_variable(space.id, "var2", "value2")

        variables = space_service.list_variables(space.id)
        assert len(variables) == 2

    def test_list_variables_by_scope(self, space_service):
        """Test listing variables filtered by scope."""
        space = space_service.create_space(name="var-space")
        space_service.set_variable(space.id, "global", "g", scope="space")
        space_service.set_variable(space.id, "local", "l", scope="document")

        space_vars = space_service.list_variables(space.id, scope="space")
        assert len(space_vars) == 1
        assert space_vars[0].name == "global"

    def test_delete_variable(self, space_service):
        """Test deleting a variable."""
        space = space_service.create_space(name="var-space")
        space_service.set_variable(space.id, "temp", "value")

        result = space_service.delete_variable(space.id, "temp")
        assert result is True

        retrieved = space_service.get_variable(space.id, "temp")
        assert retrieved is None

    def test_get_variables_dict(self, space_service):
        """Test getting variables as a dictionary."""
        space = space_service.create_space(name="var-space")
        space_service.set_variable(space.id, "api_url", "https://api.example.com")
        space_service.set_variable(space.id, "version", "2.0")

        variables_dict = space_service.get_variables_dict(space.id)
        assert variables_dict == {
            "api_url": "https://api.example.com",
            "version": "2.0",
        }

    def test_variable_with_complex_value(self, space_service):
        """Test variable with complex JSON value."""
        space = space_service.create_space(name="var-space")
        complex_value = {
            "endpoints": ["/api/v1", "/api/v2"],
            "config": {"timeout": 30},
        }
        space_service.set_variable(space.id, "api_config", complex_value)

        retrieved = space_service.get_variable(space.id, "api_config")
        assert retrieved.value == complex_value


class TestReferenceTracking:
    """Tests for transclusion reference tracking."""

    def test_add_and_get_references(self, space_service):
        """Test adding and getting references."""
        space = space_service.create_space(name="ref-space")

        space_service.add_reference("doc-1", "shared-component", space.id)
        space_service.add_reference("doc-2", "shared-component", space.id)

        refs = space_service.get_references_to("shared-component", space.id)
        assert len(refs) == 2

    def test_get_references_from(self, space_service):
        """Test getting references from a source document."""
        space = space_service.create_space(name="ref-space")

        space_service.add_reference("doc-1", "component-a", space.id)
        space_service.add_reference("doc-1", "component-b", space.id)

        refs = space_service.get_references_from("doc-1", space.id)
        assert len(refs) == 2
        targets = [r.target_doc_id for r in refs]
        assert "component-a" in targets
        assert "component-b" in targets

    def test_get_dependents(self, space_service):
        """Test getting dependent documents."""
        space = space_service.create_space(name="ref-space")

        space_service.add_reference("doc-1", "shared", space.id)
        space_service.add_reference("doc-2", "shared", space.id)
        space_service.add_reference("doc-3", "shared", space.id)

        dependents = space_service.get_dependents("shared", space.id)
        assert len(dependents) == 3
        assert set(dependents) == {"doc-1", "doc-2", "doc-3"}

    def test_clear_references_from(self, space_service):
        """Test clearing references from a source document."""
        space = space_service.create_space(name="ref-space")

        space_service.add_reference("doc-1", "a", space.id)
        space_service.add_reference("doc-1", "b", space.id)
        space_service.add_reference("doc-2", "a", space.id)

        count = space_service.clear_references_from("doc-1", space.id)
        assert count == 2

        # doc-1 refs should be gone
        refs1 = space_service.get_references_from("doc-1", space.id)
        assert len(refs1) == 0

        # doc-2 refs should still exist
        refs2 = space_service.get_references_from("doc-2", space.id)
        assert len(refs2) == 1

    def test_remove_document_clears_references(self, space_service):
        """Test that removing a document clears its references."""
        space = space_service.create_space(name="ref-space")
        doc = space_service.add_document(space.id, "/source.md")

        # Add reference from this document
        space_service.add_reference(doc.id, "target", space.id)

        # Verify reference exists
        refs = space_service.get_references_from(doc.id, space.id)
        assert len(refs) == 1

        # Remove document
        space_service.remove_document(doc.id)

        # References should be cleared
        refs = space_service.get_references_from(doc.id, space.id)
        assert len(refs) == 0


class TestFullWorkflow:
    """End-to-end workflow tests."""

    def test_documentation_space_workflow(self, space_service):
        """Test a complete documentation space workflow."""
        # Create a documentation space
        space = space_service.create_space(
            name="api-docs",
            description="API Documentation",
            config=SpaceConfig(theme="minimal"),
            metadata=SpaceMetadata(tags=["api", "v2"]),
        )

        # Add documents
        intro = space_service.add_document(
            space.id,
            "/intro.md",
            order_index=0,
            metadata={"title": "Introduction"},
        )
        endpoints = space_service.add_document(
            space.id,
            "/api/endpoints.md",
            order_index=1,
            metadata={"title": "API Endpoints"},
        )
        auth = space_service.add_document(
            space.id,
            "/api/auth.md",
            order_index=2,
            metadata={"title": "Authentication"},
        )

        # Add variables for transclusion
        space_service.set_variable(space.id, "api_base_url", "https://api.example.com")
        space_service.set_variable(space.id, "version", "2.0")

        # Track references (e.g., endpoints includes auth)
        space_service.add_reference(endpoints.id, auth.id, space.id)

        # Activate the space
        space_service.activate_space(space.id)

        # Get stats
        stats = space_service.get_space_stats(space.id)
        assert stats["document_count"] == 3
        assert stats["variable_count"] == 2
        assert stats["status"] == "active"

        # Verify the space
        retrieved = space_service.get_space(space.id)
        assert retrieved.status == SpaceStatus.ACTIVE

        # List documents in order
        docs = space_service.list_documents(space.id)
        assert len(docs) == 3
        assert docs[0].space_path == "/intro.md"

        # Get transclusion context
        context = space_service.get_variables_dict(space.id)
        assert context["api_base_url"] == "https://api.example.com"

        # Check dependencies for cache invalidation
        dependents = space_service.get_dependents(auth.id, space.id)
        assert endpoints.id in dependents

    def test_space_stats(self, space_service):
        """Test getting space statistics."""
        space = space_service.create_space(name="stats-test")
        space_service.add_document(space.id, "/doc1.md")
        space_service.add_document(space.id, "/doc2.md")
        space_service.set_variable(space.id, "var1", "value1")
        space_service.create_space(name="child", parent_space_id=space.id)

        stats = space_service.get_space_stats(space.id)

        assert stats["name"] == "stats-test"
        assert stats["document_count"] == 2
        assert stats["variable_count"] == 1
        assert stats["child_space_count"] == 1


class TestErrorHandling:
    """Tests for error handling scenarios."""

    def test_create_space_empty_name_raises(self, space_service):
        """Test that empty name raises ValueError."""
        with pytest.raises(ValueError, match="cannot be empty"):
            space_service.create_space(name="")

        with pytest.raises(ValueError, match="cannot be empty"):
            space_service.create_space(name="   ")

    def test_create_space_duplicate_name_raises(self, space_service):
        """Test that duplicate name raises ValueError."""
        space_service.create_space(name="taken")

        with pytest.raises(ValueError, match="already exists"):
            space_service.create_space(name="taken")

    def test_update_nonexistent_space_raises(self, space_service):
        """Test that updating non-existent space raises ValueError."""
        with pytest.raises(ValueError, match="not found"):
            space_service.update_space("non-existent", name="new-name")

    def test_add_document_to_nonexistent_space_raises(self, space_service):
        """Test that adding document to non-existent space raises."""
        with pytest.raises(ValueError, match="not found"):
            space_service.add_document("non-existent", "/doc.md")

    def test_set_variable_in_nonexistent_space_raises(self, space_service):
        """Test that setting variable in non-existent space raises."""
        with pytest.raises(ValueError, match="not found"):
            space_service.set_variable("non-existent", "var", "value")

    def test_create_child_with_nonexistent_parent_raises(self, space_service):
        """Test that creating child with non-existent parent raises."""
        with pytest.raises(ValueError, match="Parent space.*not found"):
            space_service.create_space(name="orphan", parent_space_id="non-existent")
