import pytest

from django_lumen.inspector import (
    DEFAULT_EXCLUDED_APPS,
    _collect_choices,
    _collect_indexes,
    get_app_models,
    get_diagram_data,
)


# ── get_app_models ────────────────────────────────────────────────────────────

class TestGetAppModels:
    def test_default_excludes_system_apps(self):
        labels = {m._meta.app_label for m in get_app_models()}
        for app in DEFAULT_EXCLUDED_APPS:
            assert app not in labels

    def test_includes_tests_app_by_default(self):
        labels = {m._meta.app_label for m in get_app_models()}
        assert 'tests' in labels

    def test_custom_excluded_removes_app(self):
        labels = {m._meta.app_label for m in get_app_models(excluded=['tests'])}
        assert 'tests' not in labels

    def test_empty_excluded_includes_all_apps(self):
        labels = {m._meta.app_label for m in get_app_models(excluded=[])}
        assert 'auth' in labels
        assert 'tests' in labels
        assert 'django_lumen' in labels

    def test_returns_list(self):
        assert isinstance(get_app_models(), list)


# ── _collect_choices ──────────────────────────────────────────────────────────

class TestCollectChoices:
    def test_field_without_choices_returns_none(self):
        from tests.models import Author
        field = Author._meta.get_field('name')
        assert _collect_choices(field) is None

    def test_text_choices_returns_all_options(self):
        from tests.models import Author
        field = Author._meta.get_field('status')
        choices = _collect_choices(field)
        assert choices is not None
        assert len(choices) == 3
        assert {'value': 'draft', 'label': 'Draft'} in choices
        assert {'value': 'published', 'label': 'Published'} in choices
        assert {'value': 'archived', 'label': 'Archived'} in choices

    def test_grouped_choices_are_flattened(self):
        class FakeField:
            choices = [
                ('Group A', [('a1', 'A One'), ('a2', 'A Two')]),
                ('Group B', [('b1', 'B One')]),
            ]

        result = _collect_choices(FakeField())
        assert len(result) == 3
        assert result[0] == {'value': 'a1', 'label': 'A One'}
        assert result[1] == {'value': 'a2', 'label': 'A Two'}
        assert result[2] == {'value': 'b1', 'label': 'B One'}

    def test_empty_choices_returns_none(self):
        class FakeField:
            choices = []

        assert _collect_choices(FakeField()) is None

    def test_field_with_no_choices_attr_returns_none(self):
        class FakeField:
            pass

        assert _collect_choices(FakeField()) is None


# ── _collect_indexes ──────────────────────────────────────────────────────────

class TestCollectIndexes:
    def test_primary_key_always_present(self):
        from tests.models import Author
        indexes = _collect_indexes(Author._meta)
        pk_entries = [i for i in indexes if i['type'] == 'pk']
        assert len(pk_entries) == 1
        assert pk_entries[0]['fields'] == ['id']
        assert pk_entries[0]['unique'] is True

    def test_unique_field_detected(self):
        from tests.models import Author
        indexes = _collect_indexes(Author._meta)
        unique_fields = [
            f for i in indexes if i['type'] == 'unique' for f in i['fields']
        ]
        assert 'email' in unique_fields

    def test_db_index_field_detected(self):
        from tests.models import Book
        indexes = _collect_indexes(Book._meta)
        index_entries = [
            i for i in indexes if i['type'] == 'index' and i['fields'] == ['title']
        ]
        assert len(index_entries) == 1
        assert index_entries[0]['unique'] is False

    def test_meta_index_with_name(self):
        from tests.models import Tag
        indexes = _collect_indexes(Tag._meta)
        named = [i for i in indexes if i['name'] == 'tag_name_idx']
        assert len(named) == 1
        assert named[0]['fields'] == ['name']

    def test_meta_index_multiple_fields(self):
        from tests.models import Book
        indexes = _collect_indexes(Book._meta)
        multi = [i for i in indexes if i['name'] == 'book_title_isbn_idx']
        assert len(multi) == 1
        assert multi[0]['fields'] == ['title', 'isbn']

    def test_unique_constraint_detected(self):
        from tests.models import Book
        indexes = _collect_indexes(Book._meta)
        named_unique = [i for i in indexes if i['name'] == 'book_title_author_uniq']
        assert len(named_unique) == 1
        assert named_unique[0]['unique'] is True
        assert 'title' in named_unique[0]['fields']
        assert 'author' in named_unique[0]['fields']

    def test_unique_field_not_duplicated_as_pk(self):
        from tests.models import Author
        indexes = _collect_indexes(Author._meta)
        pk_entries = [i for i in indexes if i['type'] == 'pk']
        # pk field (id) should appear only once
        assert len(pk_entries) == 1


# ── get_diagram_data ──────────────────────────────────────────────────────────

class TestGetDiagramData:
    def test_returns_models_key(self):
        from tests.models import Author
        data = get_diagram_data([Author])
        assert 'models' in data

    def test_single_model_produces_one_entry(self):
        from tests.models import Author
        data = get_diagram_data([Author])
        assert len(data['models']) == 1

    def test_key_format_is_app_dot_name(self):
        from tests.models import Author
        model = get_diagram_data([Author])['models'][0]
        assert model['key'] == 'tests.Author'
        assert model['name'] == 'Author'
        assert model['app'] == 'tests'

    def test_field_contains_all_required_keys(self):
        from tests.models import Author
        fields = get_diagram_data([Author])['models'][0]['fields']
        field = next(f for f in fields if f['name'] == 'name')
        for key in ('name', 'type', 'verbose_name', 'help_text', 'choices', 'relation'):
            assert key in field

    def test_help_text_is_included(self):
        from tests.models import Author
        fields = get_diagram_data([Author])['models'][0]['fields']
        name_field = next(f for f in fields if f['name'] == 'name')
        assert name_field['help_text'] == 'Full name of the author'

    def test_choices_populated_for_choice_field(self):
        from tests.models import Author
        fields = get_diagram_data([Author])['models'][0]['fields']
        status_field = next(f for f in fields if f['name'] == 'status')
        assert status_field['choices'] is not None
        assert len(status_field['choices']) == 3

    def test_choices_none_for_plain_field(self):
        from tests.models import Author
        fields = get_diagram_data([Author])['models'][0]['fields']
        name_field = next(f for f in fields if f['name'] == 'name')
        assert name_field['choices'] is None

    def test_fk_relation_populated(self):
        from tests.models import Author, Book
        models_data = get_diagram_data([Author, Book])['models']
        book = next(m for m in models_data if m['name'] == 'Book')
        author_field = next(f for f in book['fields'] if f['name'] == 'author')
        assert author_field['relation'] is not None
        assert author_field['relation']['to'] == 'tests.Author'
        assert author_field['relation']['kind'] == 'ForeignKey'

    def test_m2m_relation_populated(self):
        from tests.models import Book, Tag
        models_data = get_diagram_data([Book, Tag])['models']
        tag = next(m for m in models_data if m['name'] == 'Tag')
        books_field = next(f for f in tag['fields'] if f['name'] == 'books')
        assert books_field['relation'] is not None
        assert books_field['relation']['to'] == 'tests.Book'

    def test_non_relation_field_has_null_relation(self):
        from tests.models import Author
        fields = get_diagram_data([Author])['models'][0]['fields']
        name_field = next(f for f in fields if f['name'] == 'name')
        assert name_field['relation'] is None

    def test_indexes_key_present(self):
        from tests.models import Author
        model = get_diagram_data([Author])['models'][0]
        assert 'indexes' in model
        assert isinstance(model['indexes'], list)

    def test_models_sorted_by_app_label(self):
        from tests.models import Author, Book
        # Both in 'tests'; verify no crash and app labels are correct
        data = get_diagram_data([Book, Author])
        assert all(m['app'] == 'tests' for m in data['models'])

    def test_empty_model_list_returns_empty(self):
        data = get_diagram_data([])
        assert data == {'models': []}

    def test_two_apps_with_same_model_name_have_unique_keys(self):
        # Use auth.User and a tests model to verify key uniqueness
        from django.contrib.auth.models import User
        from tests.models import Author
        data = get_diagram_data([User, Author])
        keys = [m['key'] for m in data['models']]
        assert len(keys) == len(set(keys))
