"""
Tests for the shared module utilities: defaults, db_utils, slug_utils, utils,
and mixin dimension-to-field mappings.

These are pure unit tests that exercise the shared utility functions directly.
"""

from unittest.mock import MagicMock, patch

from django.test import TestCase, override_settings

from opencontractserver.shared.db_utils import table_has_column
from opencontractserver.shared.defaults import (
    create_model_icon_path,
    empty_text_label_position,
    jsonfield_default_value,
)
from opencontractserver.shared.mixins import (
    HasEmbeddingMixin,
    VectorSearchViaEmbeddingMixin,
)
from opencontractserver.shared.slug_utils import (
    generate_unique_slug,
    get_reserved_user_slugs,
    sanitize_slug,
    validate_user_slug_or_raise,
)
from opencontractserver.shared.utils import calc_oc_file_path


class TestDefaultFunctions(TestCase):
    """Tests for shared/defaults.py default value factory functions."""

    def test_empty_text_label_position_structure(self):
        result = empty_text_label_position()
        self.assertIsInstance(result, dict)
        self.assertIn("rects", result)
        self.assertIn("pageNumber", result)
        self.assertIn("boundingRect", result)
        self.assertEqual(result["rects"], [])
        self.assertEqual(result["pageNumber"], 1)
        br = result["boundingRect"]
        self.assertEqual(br["x1"], 0.0)
        self.assertEqual(br["x2"], 0.0)
        self.assertEqual(br["y1"], 0.0)
        self.assertEqual(br["y2"], 0.0)
        self.assertEqual(br["width"], 0.0)
        self.assertEqual(br["height"], 0.0)

    def test_empty_text_label_position_returns_new_instance(self):
        a = empty_text_label_position()
        b = empty_text_label_position()
        self.assertIsNot(a, b)

    def test_jsonfield_default_value(self):
        result = jsonfield_default_value()
        self.assertEqual(result, {})
        self.assertIsInstance(result, dict)

    def test_jsonfield_default_value_returns_new_instance(self):
        a = jsonfield_default_value()
        b = jsonfield_default_value()
        self.assertIsNot(a, b)

    def test_create_model_icon_path(self):
        mock_instance = MagicMock()
        mock_instance.creator.id = 42
        mock_instance.__class__.__name__ = "Corpus"
        result = create_model_icon_path(mock_instance, "icon.png")
        self.assertTrue(result.startswith("user_42/Corpus/icons/"))
        # Should contain a UUID
        parts = result.split("/")
        self.assertEqual(len(parts), 4)
        self.assertEqual(parts[0], "user_42")
        self.assertEqual(parts[1], "Corpus")
        self.assertEqual(parts[2], "icons")
        # UUID part should be non-empty
        self.assertTrue(len(parts[3]) > 0)

    def test_create_model_icon_path_unique(self):
        mock_instance = MagicMock()
        mock_instance.creator.id = 1
        mock_instance.__class__.__name__ = "Document"
        path1 = create_model_icon_path(mock_instance, "a.png")
        path2 = create_model_icon_path(mock_instance, "a.png")
        self.assertNotEqual(path1, path2)


class TestDbUtils(TestCase):
    """Tests for shared/db_utils.py database introspection utilities."""

    def setUp(self):
        # Clear the LRU cache between tests
        table_has_column.cache_clear()

    def test_existing_table_and_column(self):
        # users_user table exists (custom user model: AUTH_USER_MODEL = "users.User")
        result = table_has_column("users_user", "username")
        self.assertTrue(result)

    def test_existing_table_missing_column(self):
        result = table_has_column("users_user", "nonexistent_column_xyz")
        self.assertFalse(result)

    def test_nonexistent_table(self):
        result = table_has_column("completely_fake_table_xyz", "some_column")
        self.assertFalse(result)

    def test_result_is_cached(self):
        # First call
        result1 = table_has_column("users_user", "username")
        # Second call should hit cache
        result2 = table_has_column("users_user", "username")
        self.assertEqual(result1, result2)
        # Check cache info
        info = table_has_column.cache_info()
        self.assertGreaterEqual(info.hits, 1)


class TestSanitizeSlug(TestCase):
    """Tests for shared/slug_utils.py sanitize_slug function."""

    def test_basic_slug(self):
        result = sanitize_slug("Hello World", max_length=50)
        self.assertEqual(result, "Hello-World")

    def test_underscores_replaced(self):
        result = sanitize_slug("hello_world", max_length=50)
        self.assertEqual(result, "hello-world")

    def test_special_chars_removed(self):
        result = sanitize_slug("hello@world!#$%", max_length=50)
        self.assertEqual(result, "helloworld")

    def test_multiple_hyphens_collapsed(self):
        result = sanitize_slug("hello---world", max_length=50)
        self.assertEqual(result, "hello-world")

    def test_leading_trailing_hyphens_trimmed(self):
        result = sanitize_slug("--hello--", max_length=50)
        self.assertEqual(result, "hello")

    def test_max_length_enforced(self):
        result = sanitize_slug("a" * 200, max_length=10)
        self.assertEqual(len(result), 10)

    def test_empty_string(self):
        result = sanitize_slug("", max_length=50)
        self.assertEqual(result, "")

    def test_none_value(self):
        result = sanitize_slug(None, max_length=50)
        self.assertEqual(result, "")

    def test_case_preserved(self):
        result = sanitize_slug("MyDocument", max_length=50)
        self.assertEqual(result, "MyDocument")

    def test_spaces_and_underscores_mixed(self):
        result = sanitize_slug("my doc_title here", max_length=50)
        self.assertEqual(result, "my-doc-title-here")

    def test_only_special_chars(self):
        result = sanitize_slug("@#$%^&*()", max_length=50)
        self.assertEqual(result, "")

    def test_alphanumeric_with_hyphens(self):
        result = sanitize_slug("already-valid-slug-123", max_length=50)
        self.assertEqual(result, "already-valid-slug-123")


class TestGenerateUniqueSlug(TestCase):
    """Tests for shared/slug_utils.py generate_unique_slug function."""

    def _make_qs(self, existing_slugs):
        """Create a mock queryset that responds to filter().exists()."""
        qs = MagicMock()

        def filter_side_effect(**kwargs):
            slug_value = kwargs.get("slug")
            result = MagicMock()
            result.exists.return_value = slug_value in existing_slugs
            return result

        qs.filter.side_effect = filter_side_effect
        return qs

    def test_unique_slug_no_conflict(self):
        qs = self._make_qs(set())
        result = generate_unique_slug(
            base_value="My Title",
            scope_qs=qs,
            max_length=50,
            fallback_prefix="item",
        )
        self.assertEqual(result, "My-Title")

    def test_unique_slug_with_conflict(self):
        qs = self._make_qs({"My-Title"})
        result = generate_unique_slug(
            base_value="My Title",
            scope_qs=qs,
            max_length=50,
            fallback_prefix="item",
        )
        self.assertEqual(result, "My-Title-2")

    def test_unique_slug_multiple_conflicts(self):
        qs = self._make_qs({"My-Title", "My-Title-2", "My-Title-3"})
        result = generate_unique_slug(
            base_value="My Title",
            scope_qs=qs,
            max_length=50,
            fallback_prefix="item",
        )
        self.assertEqual(result, "My-Title-4")

    def test_empty_base_uses_fallback(self):
        qs = self._make_qs(set())
        result = generate_unique_slug(
            base_value="",
            scope_qs=qs,
            max_length=50,
            fallback_prefix="item",
        )
        self.assertEqual(result, "item")

    def test_special_chars_base_uses_fallback(self):
        qs = self._make_qs(set())
        result = generate_unique_slug(
            base_value="@#$%",
            scope_qs=qs,
            max_length=50,
            fallback_prefix="fallback",
        )
        self.assertEqual(result, "fallback")

    def test_max_length_with_suffix(self):
        qs = self._make_qs({"abcde"})
        result = generate_unique_slug(
            base_value="abcde",
            scope_qs=qs,
            max_length=7,
            fallback_prefix="x",
        )
        # "abcde" (5 chars) + "-2" fits within max_length=7
        self.assertEqual(result, "abcde-2")
        self.assertLessEqual(len(result), 7)


class TestValidateUserSlugOrRaise(TestCase):
    """Tests for shared/slug_utils.py validate_user_slug_or_raise function."""

    def test_valid_slug(self):
        # Should not raise
        validate_user_slug_or_raise("my-valid-slug")

    def test_valid_slug_alphanumeric(self):
        validate_user_slug_or_raise("User123")

    def test_empty_slug_raises(self):
        with self.assertRaises(ValueError) as ctx:
            validate_user_slug_or_raise("")
        self.assertIn("empty", str(ctx.exception).lower())

    def test_invalid_chars_raises(self):
        with self.assertRaises(ValueError) as ctx:
            validate_user_slug_or_raise("slug with spaces")
        self.assertIn("invalid characters", str(ctx.exception).lower())

    def test_special_chars_raises(self):
        with self.assertRaises(ValueError):
            validate_user_slug_or_raise("slug@here")

    def test_reserved_slug_raises(self):
        with self.assertRaises(ValueError) as ctx:
            validate_user_slug_or_raise("admin")
        self.assertIn("reserved", str(ctx.exception).lower())

    def test_other_reserved_slugs(self):
        reserved = ["login", "logout", "api", "graphql", "settings"]
        for slug in reserved:
            with self.assertRaises(ValueError):
                validate_user_slug_or_raise(slug)


class TestGetReservedUserSlugs(TestCase):
    """Tests for shared/slug_utils.py get_reserved_user_slugs function."""

    def test_returns_set(self):
        result = get_reserved_user_slugs()
        self.assertIsInstance(result, set)

    def test_default_includes_common_slugs(self):
        result = get_reserved_user_slugs()
        self.assertIn("admin", result)
        self.assertIn("api", result)
        self.assertIn("graphql", result)
        self.assertIn("login", result)
        self.assertIn("logout", result)

    @override_settings(RESERVED_USER_SLUGS=["custom1", "custom2"])
    def test_custom_settings_override(self):
        result = get_reserved_user_slugs()
        self.assertEqual(result, {"custom1", "custom2"})


class TestCalcOcFilePath(TestCase):
    """Tests for shared/utils.py calc_oc_file_path function."""

    def test_basic_path(self):
        mock_instance = MagicMock()
        result = calc_oc_file_path(mock_instance, "test.pdf", "documents")
        self.assertEqual(result, "uploadfiles/documents/test.pdf")

    def test_different_subfolder(self):
        mock_instance = MagicMock()
        result = calc_oc_file_path(mock_instance, "image.png", "thumbnails")
        self.assertEqual(result, "uploadfiles/thumbnails/image.png")

    def test_filename_with_uuid(self):
        mock_instance = MagicMock()
        result = calc_oc_file_path(mock_instance, "abc-123-def.pdf", "exports")
        self.assertEqual(result, "uploadfiles/exports/abc-123-def.pdf")


class TestVectorSearchMixinDimensionMapping(TestCase):
    """Tests for VectorSearchViaEmbeddingMixin._dimension_to_field mapping."""

    def setUp(self):
        self.mixin = VectorSearchViaEmbeddingMixin()
        self.mixin.EMBEDDING_RELATED_NAME = "embedding_set"

    def test_dimension_384(self):
        self.assertEqual(
            self.mixin._dimension_to_field(384), "embedding_set__vector_384"
        )

    def test_dimension_768(self):
        self.assertEqual(
            self.mixin._dimension_to_field(768), "embedding_set__vector_768"
        )

    def test_dimension_1024(self):
        self.assertEqual(
            self.mixin._dimension_to_field(1024), "embedding_set__vector_1024"
        )

    def test_dimension_1536(self):
        self.assertEqual(
            self.mixin._dimension_to_field(1536), "embedding_set__vector_1536"
        )

    def test_dimension_2048(self):
        self.assertEqual(
            self.mixin._dimension_to_field(2048), "embedding_set__vector_2048"
        )

    def test_dimension_3072(self):
        self.assertEqual(
            self.mixin._dimension_to_field(3072), "embedding_set__vector_3072"
        )

    def test_dimension_4096(self):
        self.assertEqual(
            self.mixin._dimension_to_field(4096), "embedding_set__vector_4096"
        )

    def test_unsupported_dimension_raises(self):
        with self.assertRaises(ValueError) as ctx:
            self.mixin._dimension_to_field(512)
        self.assertIn("512", str(ctx.exception))

    def test_custom_related_name(self):
        self.mixin.EMBEDDING_RELATED_NAME = "embeddings"
        self.assertEqual(self.mixin._dimension_to_field(384), "embeddings__vector_384")


class TestHasEmbeddingMixinDimensionMapping(TestCase):
    """Tests for HasEmbeddingMixin get_embedding dimension validation."""

    def test_not_implemented_error(self):
        mixin = HasEmbeddingMixin()
        with self.assertRaises(NotImplementedError):
            mixin.get_embedding_reference_kwargs()

    def test_unsupported_dimension_raises(self):
        mixin = HasEmbeddingMixin()
        with self.assertRaises(ValueError) as ctx:
            mixin.get_embedding("some-embedder", 512)
        self.assertIn("512", str(ctx.exception))

    def test_add_embedding_none_vector(self):
        mixin = HasEmbeddingMixin()
        result = mixin.add_embedding("some-embedder", None)
        self.assertIsNone(result)

    @patch(
        "opencontractserver.shared.mixins.HasEmbeddingMixin.get_embedding_reference_kwargs"
    )
    def test_get_embedding_not_found(self, mock_kwargs):
        mock_kwargs.return_value = {"document_id": 999}
        mixin = HasEmbeddingMixin()
        with patch(
            "opencontractserver.annotations.models.Embedding.objects.get"
        ) as mock_get:
            from opencontractserver.annotations.models import Embedding

            mock_get.side_effect = Embedding.DoesNotExist
            result = mixin.get_embedding("test-embedder", 384)
        self.assertIsNone(result)
