"""
Tests for item visibility in L1 web UI.

Bug found 2026-01-12: L1 run succeeded but web UI not showing:
- Runs
- Recipes
- Created media

Root causes identified:
1. Recipes: owner field filtering but owner never set in loaded recipes
2. Media: item_types table entries not created on upload/import
3. Run outputs: outputs not registered in item_types table
"""

import pytest
from pathlib import Path
from unittest.mock import MagicMock, AsyncMock, patch
import tempfile


class TestRecipeVisibility:
    """Tests for recipe listing visibility."""

    def test_recipe_filter_allows_none_owner(self) -> None:
        """
        Regression test: The recipe filter should allow recipes where owner is None.

        Bug: recipe_service.list_recipes() filtered by owner == actor_id,
        but owner field is None in recipes loaded from S-expression files.
        This caused ALL recipes to be filtered out.

        Fix: The filter is now: actor_id is None or owner is None or owner == actor_id
        """
        # Simulate the filter logic from recipe_service.list_recipes
        # OLD (broken): if actor_id is None or owner == actor_id
        # NEW (fixed): if actor_id is None or owner is None or owner == actor_id

        test_cases = [
            # (actor_id, owner, expected_visible, description)
            (None, None, True, "No filter, no owner -> visible"),
            (None, "@someone@example.com", True, "No filter, has owner -> visible"),
            ("@testuser@example.com", None, True, "Has filter, no owner -> visible (shared)"),
            ("@testuser@example.com", "@testuser@example.com", True, "Filter matches owner -> visible"),
            ("@testuser@example.com", "@other@example.com", False, "Filter doesn't match -> hidden"),
        ]

        for actor_id, owner, expected_visible, description in test_cases:
            # This is the FIXED filter logic from recipe_service.py line 86
            is_visible = actor_id is None or owner is None or owner == actor_id

            assert is_visible == expected_visible, f"Failed: {description}"

    def test_recipe_filter_old_logic_was_broken(self) -> None:
        """Document that the old filter logic excluded all recipes with owner=None."""
        # OLD filter: actor_id is None or owner == actor_id
        # This broke when owner=None and actor_id was provided

        actor_id = "@testuser@example.com"
        owner = None  # This is what compiled sexp produces

        # OLD logic (broken):
        old_logic_visible = actor_id is None or owner == actor_id
        assert old_logic_visible is False, "Old logic incorrectly hid owner=None recipes"

        # NEW logic (fixed):
        new_logic_visible = actor_id is None or owner is None or owner == actor_id
        assert new_logic_visible is True, "New logic should show owner=None recipes"


class TestMediaVisibility:
    """Tests for media visibility after upload."""

    @pytest.mark.asyncio
    async def test_upload_content_creates_item_type_record(self) -> None:
        """
        Test: Uploaded media must be registered in item_types table via save_item_metadata.

        The save_item_metadata function creates entries in item_types table,
        enabling the media to appear in list_media queries.
        """
        import importlib.util
        spec = importlib.util.spec_from_file_location(
            "cache_service",
            "/home/giles/art/art-celery/app/services/cache_service.py"
        )
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        CacheService = module.CacheService

        # Create mocks
        mock_db = AsyncMock()
        mock_db.create_cache_item = AsyncMock()
        mock_db.save_item_metadata = AsyncMock()

        mock_cache = MagicMock()
        cached_result = MagicMock()
        cached_result.cid = "QmUploadedContent123"
        mock_cache.put.return_value = (cached_result, "QmIPFSCid123")

        service = CacheService(database=mock_db, cache_manager=mock_cache)

        # Upload content
        cid, ipfs_cid, error = await service.upload_content(
            content=b"test video content",
            filename="test.mp4",
            actor_id="@testuser@example.com",
        )

        assert error is None, f"Upload failed: {error}"
        assert cid is not None

        # Verify save_item_metadata was called (which creates item_types entry)
        mock_db.save_item_metadata.assert_called_once()

        # Verify it was called with correct actor_id and a media type (not mime type)
        call_kwargs = mock_db.save_item_metadata.call_args[1]
        assert call_kwargs.get('actor_id') == "@testuser@example.com", \
            "save_item_metadata must be called with the uploading user's actor_id"
        # item_type should be media category like "video", "image", "audio", "unknown"
        # NOT mime type like "video/mp4"
        item_type = call_kwargs.get('item_type')
        assert item_type in ("video", "image", "audio", "unknown"), \
            f"item_type should be media category, got '{item_type}'"

    @pytest.mark.asyncio
    async def test_import_from_ipfs_creates_item_type_record(self) -> None:
        """
        Test: Imported media must be registered in item_types table via save_item_metadata.

        The save_item_metadata function creates entries in item_types table with
        detected media type, enabling the media to appear in list_media queries.
        """
        import importlib.util
        spec = importlib.util.spec_from_file_location(
            "cache_service",
            "/home/giles/art/art-celery/app/services/cache_service.py"
        )
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        CacheService = module.CacheService

        mock_db = AsyncMock()
        mock_db.create_cache_item = AsyncMock()
        mock_db.save_item_metadata = AsyncMock()

        mock_cache = MagicMock()
        cached_result = MagicMock()
        cached_result.cid = "QmImportedContent123"
        mock_cache.put.return_value = (cached_result, "QmIPFSCid456")

        service = CacheService(database=mock_db, cache_manager=mock_cache)
        service.cache_dir = Path(tempfile.gettempdir())

        # We need to mock the ipfs_client module at the right location
        import importlib
        ipfs_module = MagicMock()
        ipfs_module.get_file = MagicMock(return_value=True)

        # Patch at module level
        with patch.dict('sys.modules', {'ipfs_client': ipfs_module}):
            # Import from IPFS
            cid, error = await service.import_from_ipfs(
                ipfs_cid="QmSourceIPFSCid",
                actor_id="@testuser@example.com",
            )

        # Verify save_item_metadata was called (which creates item_types entry)
        mock_db.save_item_metadata.assert_called_once()

        # Verify it was called with detected media type (not hardcoded "media")
        call_kwargs = mock_db.save_item_metadata.call_args[1]
        item_type = call_kwargs.get('item_type')
        assert item_type in ("video", "image", "audio", "unknown"), \
            f"item_type should be detected media category, got '{item_type}'"


class TestRunOutputVisibility:
    """Tests for run output visibility."""

    @pytest.mark.asyncio
    async def test_completed_run_output_visible_in_media_list(self) -> None:
        """
        Run outputs should be accessible in media listings.

        When a run completes, its output should be registered in item_types
        so it appears in the user's media gallery.
        """
        # This test documents the expected behavior
        # Run outputs are stored in run_cache but should also be in item_types
        # for the media gallery to show them

        # The fix should either:
        # 1. Add item_types entry when run completes, OR
        # 2. Modify list_media to also check run_cache outputs
        pass  # Placeholder for implementation test


class TestDatabaseItemTypes:
    """Tests for item_types database operations."""

    @pytest.mark.asyncio
    async def test_add_item_type_function_exists(self) -> None:
        """Verify add_item_type function exists and has correct signature."""
        import database

        assert hasattr(database, 'add_item_type'), \
            "database.add_item_type function should exist"

        # Check it's an async function
        import inspect
        assert inspect.iscoroutinefunction(database.add_item_type), \
            "add_item_type should be an async function"

    @pytest.mark.asyncio
    async def test_get_user_items_returns_items_from_item_types(self) -> None:
        """
        Verify get_user_items queries item_types table.

        If item_types has no entries for a user, they see no media.
        """
        # This is a documentation test showing the data flow:
        # 1. User uploads content -> should create item_types entry
        # 2. list_media -> calls get_user_items -> queries item_types
        # 3. If step 1 didn't create item_types entry, step 2 returns empty
        pass


class TestTemplateRendering:
    """Tests for template variable passing."""

    def test_cache_not_found_template_receives_content_hash(self) -> None:
        """
        Regression test: cache/not_found.html template requires content_hash.

        Bug: The template uses {{ content_hash[:24] }} but the route
        doesn't pass content_hash to the render context.

        Error: jinja2.exceptions.UndefinedError: 'content_hash' is undefined
        """
        # This test documents the bug - the template expects content_hash
        # but the route at /app/app/routers/cache.py line 57 doesn't provide it
        pass  # Will verify fix by checking route code


class TestOwnerFieldInRecipes:
    """Tests for owner field handling in recipes."""

    def test_sexp_recipe_has_none_owner_by_default(self) -> None:
        """
        S-expression recipes have owner=None by default.

        The compiled recipe includes owner field but it's None,
        so the list_recipes filter must allow owner=None to show
        shared/public recipes.
        """
        sample_sexp = """
        (recipe "test"
          (-> (source :input true :name "video")
              (fx identity)))
        """

        from artdag.sexp import compile_string

        compiled = compile_string(sample_sexp)
        recipe_dict = compiled.to_dict()

        # The compiled recipe has owner field but it's None
        assert recipe_dict.get("owner") is None, \
            "Compiled S-expression should have owner=None"

        # This means the filter must allow owner=None for recipes to be visible
        # The fix: if actor_id is None or owner is None or owner == actor_id
