"""
Stress tests for mizan.shapes — edge cases and deep nesting.

Models: Publisher → Author → Book → Chapter → Section (5 levels deep),
two FKs to same model, slug PK, UUID PK, self-referential FK, M2M,
nullable FKs, abstract bases, empty/zero/false values.
"""

import pytest
from typing import get_type_hints

from django.test import TestCase

from mizan.shapes import Shape, Diff, NestedDiff

import uuid

from tests.models import (
    Publisher,
    Author,
    Book,
    Chapter,
    Section,
    Tag,
    Category,
)


# =============================================================================
# Shapes — varying projections
# =============================================================================


class TagShape(Shape[Tag]):
    slug: str
    label: str


class FlatAuthorShape(Shape[Author]):
    id: int | None = None
    name: str


class FlatBookShape(Shape[Book]):
    id: int | None = None
    title: str
    is_published: bool


class BookCardShape(Shape[Book]):
    id: int | None = None
    title: str
    isbn: str
    page_count: int
    is_published: bool
    author: FlatAuthorShape  # single nested, not list


class AuthorCardShape(Shape[Author]):
    id: int | None = None
    name: str
    bio: str
    books: list[FlatBookShape] = []


class SectionShape(Shape[Section]):
    id: uuid.UUID | None = None
    heading: str
    body: str
    position: int


class ChapterShape(Shape[Chapter]):
    id: int | None = None
    number: int
    title: str
    word_count: int
    sections: list[SectionShape] = []


class BookDetailShape(Shape[Book]):
    id: int | None = None
    title: str
    isbn: str
    page_count: int
    is_published: bool
    author: FlatAuthorShape
    chapters: list[ChapterShape] = []
    tags: list[TagShape] = []


class AuthorDetailShape(Shape[Author]):
    id: int | None = None
    name: str
    bio: str
    books: list[BookDetailShape] = []


class PublisherDetailShape(Shape[Publisher]):
    id: int | None = None
    name: str
    country: str
    authors: list[AuthorDetailShape] = []


class BookWithEditorShape(Shape[Book]):
    """Two FKs to the same model (author + editor)."""

    id: int | None = None
    title: str
    author: FlatAuthorShape
    editor: FlatAuthorShape | None = None


class CategoryShape(Shape[Category]):
    id: int | None = None
    name: str
    children: list["CategoryShape"] = []


# =============================================================================
# Shape class creation
# =============================================================================


class TestShapeClassCreation(TestCase):
    def test_flat_shape_has_no_nested(self):
        self.assertEqual(FlatAuthorShape._nested, {})
        self.assertEqual(FlatAuthorShape._field_names, ["id", "name"])

    def test_nested_shape_detected(self):
        self.assertIn("books", AuthorCardShape._nested)
        self.assertIs(AuthorCardShape._nested["books"], FlatBookShape)

    def test_deep_nesting_spec_depth(self):
        """PublisherDetailShape → Author → Book → Chapter → Section."""
        nested_keys = {
            k for d in PublisherDetailShape._spec if isinstance(d, dict) for k in d
        }
        self.assertIn("authors", nested_keys)

        author_spec = next(
            d["authors"]
            for d in PublisherDetailShape._spec
            if isinstance(d, dict) and "authors" in d
        )
        author_nested = {k for d in author_spec if isinstance(d, dict) for k in d}
        self.assertIn("books", author_nested)

    def test_pk_field_resolution_integer(self):
        self.assertEqual(FlatAuthorShape._pk_field, "id")

    def test_pk_field_resolution_slug(self):
        self.assertEqual(TagShape._pk_field, "slug")

    def test_pk_field_resolution_uuid(self):
        self.assertEqual(SectionShape._pk_field, "id")

    def test_single_nested_not_list(self):
        self.assertIn("author", BookCardShape._nested)
        self.assertIs(BookCardShape._nested["author"], FlatAuthorShape)

    def test_optional_nested(self):
        """BookWithEditorShape.editor is FlatAuthorShape | None.
        _extract_shape_class needs to handle Optional/Union."""
        # If this doesn't detect editor as nested, it's a known gap
        if "editor" in BookWithEditorShape._nested:
            self.assertIs(BookWithEditorShape._nested["editor"], FlatAuthorShape)
        else:
            self.skipTest(
                "_extract_shape_class does not unwrap Optional[Shape] — known gap"
            )

    def test_self_referential_shape(self):
        """CategoryShape.children references itself."""
        self.assertIn("children", CategoryShape._nested)
        self.assertIs(CategoryShape._nested["children"], CategoryShape)

    def test_multiple_shapes_same_model_independent(self):
        self.assertLess(
            len(FlatBookShape._field_names), len(BookDetailShape._field_names)
        )
        self.assertNotEqual(FlatBookShape._spec, BookDetailShape._spec)


# =============================================================================
# Queries
# =============================================================================


class TestShapeQuery(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.publisher = Publisher.objects.create(name="Orbit", country="UK")
        cls.mentor = Author.objects.create(
            name="Ursula", bio="Legend", publisher=cls.publisher
        )
        cls.author = Author.objects.create(
            name="Ann Leckie",
            bio="Imperial Radch",
            publisher=cls.publisher,
            mentor=cls.mentor,
        )
        cls.editor = Author.objects.create(
            name="Devi Pillai", bio="Editor", publisher=cls.publisher
        )
        cls.tag_sf = Tag.objects.create(slug="sci-fi", label="Science Fiction")
        cls.tag_space = Tag.objects.create(slug="space-opera", label="Space Opera")

        cls.book = Book.objects.create(
            title="Ancillary Justice",
            isbn="9780316246620",
            page_count=386,
            is_published=True,
            author=cls.author,
            editor=cls.editor,
        )
        cls.book.tags.add(cls.tag_sf, cls.tag_space)

        cls.ch1 = Chapter.objects.create(
            book=cls.book, number=1, title="The Body", word_count=5200
        )
        cls.ch2 = Chapter.objects.create(
            book=cls.book, number=2, title="The Ship", word_count=4800
        )
        Section.objects.create(
            chapter=cls.ch1, heading="Opening", body="...", position=0
        )
        Section.objects.create(
            chapter=cls.ch1, heading="Discovery", body="...", position=1
        )

        cls.root_cat = Category.objects.create(name="Fiction")
        cls.child_cat = Category.objects.create(name="Sci-Fi", parent=cls.root_cat)
        Category.objects.create(name="Hard SF", parent=cls.child_cat)

    # ── Flat ──

    def test_flat_query_returns_minimal_fields(self):
        results = FlatAuthorShape.query()
        self.assertEqual(len(results), 3)
        for r in results:
            self.assertTrue(hasattr(r, "name"))
            self.assertTrue(hasattr(r, "id"))

    def test_flat_query_with_lambda_filter(self):
        results = FlatAuthorShape.query(lambda qs: qs.filter(name="Ann Leckie"))
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].name, "Ann Leckie")

    def test_flat_query_with_raw_queryset(self):
        qs = Author.objects.filter(mentor__isnull=False)
        results = FlatAuthorShape.query(qs)
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].name, "Ann Leckie")

    # ── Nested ──

    def test_single_nested_fk(self):
        results = BookCardShape.query(lambda qs: qs.filter(pk=self.book.pk))
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].author.name, "Ann Leckie")

    def test_list_nested_reverse_fk(self):
        results = AuthorCardShape.query(lambda qs: qs.filter(pk=self.author.pk))
        self.assertEqual(len(results), 1)
        self.assertEqual(len(results[0].books), 1)
        self.assertEqual(results[0].books[0].title, "Ancillary Justice")

    def test_deep_nesting_book_chapters_sections(self):
        results = BookDetailShape.query(lambda qs: qs.filter(pk=self.book.pk))
        self.assertEqual(len(results), 1)
        book = results[0]
        self.assertEqual(len(book.chapters), 2)
        ch1 = next(c for c in book.chapters if c.number == 1)
        self.assertEqual(len(ch1.sections), 2)

    def test_full_depth_publisher_to_section(self):
        """5 levels: Publisher → Author → Book → Chapter → Section."""
        results = PublisherDetailShape.query(lambda qs: qs.filter(pk=self.publisher.pk))
        self.assertEqual(len(results), 1)
        pub = results[0]
        self.assertEqual(len(pub.authors), 3)
        leckie = next(a for a in pub.authors if a.name == "Ann Leckie")
        self.assertEqual(len(leckie.books), 1)
        self.assertEqual(len(leckie.books[0].chapters), 2)

    def test_two_fks_to_same_model(self):
        results = BookWithEditorShape.query(lambda qs: qs.filter(pk=self.book.pk))
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].author.name, "Ann Leckie")
        if "editor" in BookWithEditorShape._nested:
            self.assertIsNotNone(results[0].editor)
            self.assertEqual(results[0].editor.name, "Devi Pillai")

    def test_nullable_fk_returns_none(self):
        book_no_editor = Book.objects.create(
            title="Provenance",
            isbn="9780316246699",
            page_count=448,
            is_published=True,
            author=self.author,
            editor=None,
        )
        results = BookWithEditorShape.query(lambda qs: qs.filter(pk=book_no_editor.pk))
        self.assertEqual(len(results), 1)
        if "editor" in BookWithEditorShape._nested:
            self.assertIsNone(results[0].editor)

    def test_m2m_tags(self):
        results = BookDetailShape.query(lambda qs: qs.filter(pk=self.book.pk))
        book = results[0]
        self.assertEqual(len(book.tags), 2)
        slugs = {t.slug for t in book.tags}
        self.assertEqual(slugs, {"sci-fi", "space-opera"})

    def test_slug_pk_shape(self):
        results = TagShape.query()
        self.assertEqual(len(results), 2)
        self.assertTrue(all(isinstance(r.slug, str) for r in results))

    def test_relation_qs_filters_nested(self):
        results = AuthorCardShape.query(
            lambda qs: qs.filter(pk=self.author.pk),
            books=lambda qs: qs.filter(is_published=True),
        )
        self.assertEqual(len(results), 1)
        self.assertTrue(all(b.is_published for b in results[0].books))

    def test_empty_nested_list(self):
        results = AuthorCardShape.query(lambda qs: qs.filter(pk=self.editor.pk))
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].books, [])

    # ── Query efficiency ──

    def test_flat_query_is_single_query(self):
        with self.assertNumQueries(1):
            FlatAuthorShape.query()

    def test_nested_query_uses_prefetch(self):
        with self.assertNumQueries(2):
            AuthorCardShape.query()


# =============================================================================
# Diff
# =============================================================================


class TestDiff(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.publisher = Publisher.objects.create(name="Tor", country="US")
        cls.author = Author.objects.create(
            name="Brandon Sanderson", bio="Cosmere", publisher=cls.publisher
        )
        cls.book = Book.objects.create(
            title="Mistborn",
            isbn="9780765311788",
            page_count=541,
            is_published=True,
            author=cls.author,
        )
        cls.ch1 = Chapter.objects.create(
            book=cls.book, number=1, title="Ash", word_count=6000
        )
        cls.ch2 = Chapter.objects.create(
            book=cls.book, number=2, title="Mist", word_count=5500
        )

    # ── Single item ──

    def test_diff_no_changes(self):
        shape = BookCardShape(
            id=self.book.pk,
            title="Mistborn",
            isbn="9780765311788",
            page_count=541,
            is_published=True,
            author=FlatAuthorShape(id=self.author.pk, name="Brandon Sanderson"),
        )
        d = shape.diff()
        self.assertFalse(d.is_new)
        self.assertEqual(d.changed, {})

    def test_diff_detects_field_change(self):
        shape = BookCardShape(
            id=self.book.pk,
            title="Mistborn: The Final Empire",
            isbn="9780765311788",
            page_count=541,
            is_published=True,
            author=FlatAuthorShape(id=self.author.pk, name="Brandon Sanderson"),
        )
        d = shape.diff()
        self.assertIn("title", d.changed)
        self.assertEqual(d.changed["title"], "Mistborn: The Final Empire")

    def test_diff_multiple_field_changes(self):
        shape = BookCardShape(
            id=self.book.pk,
            title="Mistborn: TFE",
            isbn="9780765311788",
            page_count=600,
            is_published=True,
            author=FlatAuthorShape(id=self.author.pk, name="Brandon Sanderson"),
        )
        d = shape.diff()
        self.assertIn("title", d.changed)
        self.assertIn("page_count", d.changed)
        self.assertNotIn("isbn", d.changed)

    def test_diff_new_item(self):
        shape = FlatBookShape(id=None, title="Elantris", is_published=True)
        d = shape.diff()
        self.assertTrue(d.is_new)
        self.assertIn("title", d.changed)

    def test_diff_nonexistent_pk_raises(self):
        shape = FlatBookShape(id=999999, title="Nope", is_published=False)
        with self.assertRaises(Book.DoesNotExist):
            shape.diff()

    # ── Nested ──

    def test_nested_diff_detects_updated_chapter(self):
        shape = BookDetailShape(
            id=self.book.pk,
            title="Mistborn",
            isbn="9780765311788",
            page_count=541,
            is_published=True,
            author=FlatAuthorShape(id=self.author.pk, name="Brandon Sanderson"),
            chapters=[
                ChapterShape(
                    id=self.ch1.pk,
                    number=1,
                    title="Ash Falls",
                    word_count=6000,
                    sections=[],
                ),
                ChapterShape(
                    id=self.ch2.pk, number=2, title="Mist", word_count=5500, sections=[]
                ),
            ],
            tags=[],
        )
        d = shape.diff()
        self.assertEqual(len(d.chapters.updated), 1)
        self.assertEqual(d.chapters.updated[0].title, "Ash Falls")

    def test_nested_diff_detects_created(self):
        shape = BookDetailShape(
            id=self.book.pk,
            title="Mistborn",
            isbn="9780765311788",
            page_count=541,
            is_published=True,
            author=FlatAuthorShape(id=self.author.pk, name="Brandon Sanderson"),
            chapters=[
                ChapterShape(
                    id=self.ch1.pk, number=1, title="Ash", word_count=6000, sections=[]
                ),
                ChapterShape(
                    id=self.ch2.pk, number=2, title="Mist", word_count=5500, sections=[]
                ),
                ChapterShape(
                    id=None, number=3, title="New Chapter", word_count=0, sections=[]
                ),
            ],
            tags=[],
        )
        d = shape.diff()
        self.assertEqual(len(d.chapters.created), 1)

    def test_nested_diff_detects_deleted(self):
        shape = BookDetailShape(
            id=self.book.pk,
            title="Mistborn",
            isbn="9780765311788",
            page_count=541,
            is_published=True,
            author=FlatAuthorShape(id=self.author.pk, name="Brandon Sanderson"),
            chapters=[
                ChapterShape(
                    id=self.ch1.pk, number=1, title="Ash", word_count=6000, sections=[]
                ),
            ],
            tags=[],
        )
        d = shape.diff()
        self.assertIn(self.ch2.pk, d.chapters.deleted)

    def test_nested_diff_combined_operations(self):
        shape = BookDetailShape(
            id=self.book.pk,
            title="Mistborn",
            isbn="9780765311788",
            page_count=541,
            is_published=True,
            author=FlatAuthorShape(id=self.author.pk, name="Brandon Sanderson"),
            chapters=[
                ChapterShape(
                    id=self.ch1.pk,
                    number=1,
                    title="Ash Rewritten",
                    word_count=7000,
                    sections=[],
                ),
                ChapterShape(
                    id=None, number=3, title="Epilogue", word_count=2000, sections=[]
                ),
            ],
            tags=[],
        )
        d = shape.diff()
        self.assertEqual(len(d.chapters.updated), 1)
        self.assertEqual(len(d.chapters.deleted), 1)
        self.assertEqual(len(d.chapters.created), 1)

    # ── Strict Diff access ──

    def test_diff_strict_getattr_raises_on_typo(self):
        shape = FlatBookShape(id=self.book.pk, title="Mistborn", is_published=True)
        d = shape.diff()
        with self.assertRaises(AttributeError):
            _ = d.chapterz

    def test_diff_strict_nested_raises_on_typo(self):
        shape = FlatBookShape(id=self.book.pk, title="Mistborn", is_published=True)
        d = shape.diff()
        with self.assertRaises(KeyError):
            d.nested("chapterz")

    def test_diff_strict_shows_valid_names(self):
        shape = BookDetailShape(
            id=self.book.pk,
            title="Mistborn",
            isbn="9780765311788",
            page_count=541,
            is_published=True,
            author=FlatAuthorShape(id=self.author.pk, name="Brandon Sanderson"),
            chapters=[],
            tags=[],
        )
        d = shape.diff()
        with self.assertRaises(AttributeError) as ctx:
            _ = d.bogus
        self.assertIn("chapters", str(ctx.exception))

    # ── diff_many ──

    def test_diff_many_single_query_for_existing(self):
        items = [FlatBookShape(id=self.book.pk, title="Renamed", is_published=True)]
        results = FlatBookShape.diff_many(items)
        self.assertEqual(len(results), 1)
        _, d = results[0]
        self.assertIn("title", d.changed)

    def test_diff_many_mixed_new_and_existing(self):
        items = [
            FlatBookShape(id=self.book.pk, title="Mistborn", is_published=True),
            FlatBookShape(id=None, title="New Book", is_published=False),
        ]
        results = FlatBookShape.diff_many(items)
        new = [d for _, d in results if d.is_new]
        existing = [d for _, d in results if not d.is_new]
        self.assertEqual(len(new), 1)
        self.assertEqual(len(existing), 1)

    def test_diff_many_nonexistent_raises(self):
        items = [FlatBookShape(id=999999, title="Ghost", is_published=False)]
        with self.assertRaises(Book.DoesNotExist):
            FlatBookShape.diff_many(items)

    def test_diff_many_batched_query(self):
        book2 = Book.objects.create(
            title="Warbreaker",
            isbn="9780765320308",
            page_count=592,
            is_published=True,
            author=self.author,
        )
        items = [
            FlatBookShape(id=self.book.pk, title="Mistborn", is_published=True),
            FlatBookShape(id=book2.pk, title="Warbreaker Updated", is_published=True),
        ]
        with self.assertNumQueries(1):
            FlatBookShape.diff_many(items)

    def test_diff_many_empty(self):
        self.assertEqual(FlatBookShape.diff_many([]), [])


# =============================================================================
# Edge cases
# =============================================================================


class TestEdgeCases(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.publisher = Publisher.objects.create(name="Edge Cases Ltd", country="XX")
        cls.author = Author.objects.create(
            name="Edge Author", bio="", publisher=cls.publisher
        )

    def test_empty_table_returns_empty_list(self):
        Tag.objects.all().delete()
        results = TagShape.query()
        self.assertEqual(results, [])

    def test_empty_string_fields(self):
        results = AuthorCardShape.query(lambda qs: qs.filter(pk=self.author.pk))
        self.assertEqual(results[0].bio, "")

    def test_boolean_false_is_not_missing(self):
        book = Book.objects.create(
            title="Unpublished",
            isbn="0000000000000",
            page_count=0,
            is_published=False,
            author=self.author,
        )
        results = FlatBookShape.query(lambda qs: qs.filter(pk=book.pk))
        self.assertIs(results[0].is_published, False)

    def test_zero_integer_is_not_missing(self):
        book = Book.objects.create(
            title="Empty",
            isbn="0000000000001",
            page_count=0,
            is_published=False,
            author=self.author,
        )
        results = BookCardShape.query(lambda qs: qs.filter(pk=book.pk))
        self.assertEqual(results[0].page_count, 0)

    def test_large_queryset(self):
        books = [
            Book(
                title=f"Book {i}",
                isbn=f"{i:013d}",
                page_count=i * 10,
                is_published=i % 2 == 0,
                author=self.author,
            )
            for i in range(100)
        ]
        Book.objects.bulk_create(books)
        results = FlatBookShape.query(lambda qs: qs.filter(author=self.author))
        self.assertGreaterEqual(len(results), 100)

    def test_diff_on_boolean_change(self):
        book = Book.objects.create(
            title="Toggle",
            isbn="1111111111111",
            page_count=100,
            is_published=False,
            author=self.author,
        )
        shape = FlatBookShape(id=book.pk, title="Toggle", is_published=True)
        d = shape.diff()
        self.assertIn("is_published", d.changed)
        self.assertIs(d.changed["is_published"], True)

    def test_diff_unchanged_returns_empty(self):
        book = Book.objects.create(
            title="Same",
            isbn="2222222222222",
            page_count=200,
            is_published=True,
            author=self.author,
        )
        shape = FlatBookShape(id=book.pk, title="Same", is_published=True)
        d = shape.diff()
        self.assertEqual(d.changed, {})
        self.assertFalse(d.is_new)
