import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import { createVuetify } from 'vuetify';
import axios from 'axios';

vi.mock('axios');

import ProjectsView from '../../resources/js/views/ProjectsView.vue';

// VDialog в JSDOM не рендерит в teleport-цели; стабом отключаем диалоги,
// чтобы не падал mount при попытке портала.
const factory = () =>
    mount(ProjectsView, {
        global: {
            plugins: [createVuetify()],
            stubs: {
                VDialog: {
                    template: '<div class="dialog-stub" v-if="modelValue"><slot /></div>',
                    props: ['modelValue'],
                },
                NewProjectDialog: true,
                EditProjectDialog: true,
            },
        },
    });

beforeEach(() => {
    setActivePinia(createPinia());
    vi.clearAllMocks();
});

describe('ProjectsView', () => {
    it('shows empty state when no projects', async () => {
        (axios.get as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
            data: { data: [], meta: { total: 0, current_page: 1, per_page: 20 } },
        });
        const wrapper = factory();
        await flushPromises();
        expect(wrapper.text()).toMatch(/нет проектов|empty/i);
    });

    it('renders card for each project', async () => {
        (axios.get as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
            data: {
                data: [
                    {
                        id: 1,
                        name: 'AlphaSite',
                        signal_type: 'site',
                        signal_identifier: 'a.ru',
                        daily_limit_target: 10,
                        delivered_today: 0,
                        is_active: true,
                        sync_status: 'ok',
                    },
                ],
                meta: { total: 1, current_page: 1, per_page: 20 },
            },
        });
        const wrapper = factory();
        await flushPromises();
        expect(wrapper.text()).toContain('AlphaSite');
    });

    it.skip('filter by signal_type refetches', async () => {
        // TODO: VSelect dropdown в jsdom не открывает items-list через teleport,
        // findComponent({name:'VSelect'}).vm.$emit некорректно тригерит реактивную
        // цепочку @update:model-value. Покрытие — через Histoire + e2e после Plan 5.
    });

    it('shows BulkActionsBar when at least 2 selected', async () => {
        (axios.get as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
            data: {
                data: [
                    {
                        id: 1,
                        name: 'A',
                        signal_type: 'site',
                        signal_identifier: 'a.ru',
                        daily_limit_target: 10,
                        delivered_today: 0,
                        is_active: true,
                        sync_status: 'ok',
                    },
                    {
                        id: 2,
                        name: 'B',
                        signal_type: 'site',
                        signal_identifier: 'b.ru',
                        daily_limit_target: 10,
                        delivered_today: 0,
                        is_active: true,
                        sync_status: 'ok',
                    },
                ],
                meta: { total: 2, current_page: 1, per_page: 20 },
            },
        });
        const wrapper = factory();
        await flushPromises();
        const cards = wrapper.findAllComponents({ name: 'ProjectCard' });
        expect(cards.length).toBe(2);
        cards[0].vm.$emit('toggle-select', 1);
        cards[1].vm.$emit('toggle-select', 2);
        await wrapper.vm.$nextTick();
        expect(wrapper.findComponent({ name: 'BulkActionsBar' }).exists()).toBe(true);
    });
});

// Integration tests for the Task 8 contract: drawer × bulk-bar mutual exclusion.
// Contract:
//   - 0 selected → no drawer open, no bulk-bar, no .has-drawer class.
//   - 1 selected → drawer open, bulk-bar hidden, .has-drawer class on view.
//   - ≥2 selected → drawer NOT open, bulk-bar visible, no .has-drawer class.
//   - Drawer @close → store.clearSelection() → both hidden.
//   - singleSelectedProject = null when selected id not in items.
describe('ProjectsView × ProjectDetailsDrawer integration', () => {
    const projectA = {
        id: 1,
        name: 'A',
        signal_type: 'site' as const,
        signal_identifier: 'a.ru',
        daily_limit_target: 10,
        delivered_today: 0,
        is_active: true,
        sync_status: 'ok' as const,
    };
    const projectB = {
        id: 2,
        name: 'B',
        signal_type: 'site' as const,
        signal_identifier: 'b.ru',
        daily_limit_target: 10,
        delivered_today: 0,
        is_active: true,
        sync_status: 'ok' as const,
    };

    function mockProjects(items: Array<Record<string, unknown>>) {
        (axios.get as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
            data: {
                data: items,
                meta: { total: items.length, current_page: 1, per_page: 20 },
            },
        });
    }

    it('0 selected → no drawer open, no bulk-bar, no .has-drawer class', async () => {
        mockProjects([projectA, projectB]);
        const wrapper = factory();
        await flushPromises();
        // Drawer aside element always renders, but should not have .open class.
        const drawer = wrapper.find('aside.project-details-drawer');
        expect(drawer.exists()).toBe(true);
        expect(drawer.classes()).not.toContain('open');
        // BulkActionsBar uses v-if; should not exist.
        expect(wrapper.findComponent({ name: 'BulkActionsBar' }).exists()).toBe(false);
        // .has-drawer class should not be on the view root.
        expect(wrapper.find('.projects-view').classes()).not.toContain('has-drawer');
    });

    it('1 selected → drawer open, bulk-bar hidden, .has-drawer class present', async () => {
        mockProjects([projectA, projectB]);
        const wrapper = factory();
        await flushPromises();
        const cards = wrapper.findAllComponents({ name: 'ProjectCard' });
        expect(cards.length).toBe(2);
        cards[0].vm.$emit('toggle-select', 1);
        await wrapper.vm.$nextTick();
        // Drawer should be open.
        const drawer = wrapper.find('aside.project-details-drawer');
        expect(drawer.exists()).toBe(true);
        expect(drawer.classes()).toContain('open');
        // BulkActionsBar should NOT exist (size < 2).
        expect(wrapper.findComponent({ name: 'BulkActionsBar' }).exists()).toBe(false);
        // .has-drawer class should be present.
        expect(wrapper.find('.projects-view').classes()).toContain('has-drawer');
    });

    it('2 selected → drawer NOT open, bulk-bar visible, no .has-drawer class', async () => {
        mockProjects([projectA, projectB]);
        const wrapper = factory();
        await flushPromises();
        const cards = wrapper.findAllComponents({ name: 'ProjectCard' });
        expect(cards.length).toBe(2);
        cards[0].vm.$emit('toggle-select', 1);
        cards[1].vm.$emit('toggle-select', 2);
        await wrapper.vm.$nextTick();
        // Drawer should NOT be open (singleSelectedProject = null when size != 1).
        const drawer = wrapper.find('aside.project-details-drawer');
        expect(drawer.exists()).toBe(true);
        expect(drawer.classes()).not.toContain('open');
        // BulkActionsBar should exist (size >= 2).
        expect(wrapper.findComponent({ name: 'BulkActionsBar' }).exists()).toBe(true);
        // .has-drawer class should NOT be present.
        expect(wrapper.find('.projects-view').classes()).not.toContain('has-drawer');
    });

    it('drawer @close → store.clearSelection → both bulk-bar and drawer hidden', async () => {
        mockProjects([projectA, projectB]);
        const wrapper = factory();
        await flushPromises();
        const cards = wrapper.findAllComponents({ name: 'ProjectCard' });
        cards[0].vm.$emit('toggle-select', 1);
        await wrapper.vm.$nextTick();
        // Sanity: drawer is open with 1 selected.
        const drawerBefore = wrapper.find('aside.project-details-drawer');
        expect(drawerBefore.classes()).toContain('open');
        // Emit close from drawer; onDrawerClose handler calls store.clearSelection().
        const drawerComp = wrapper.findComponent({ name: 'ProjectDetailsDrawer' });
        expect(drawerComp.exists()).toBe(true);
        drawerComp.vm.$emit('close');
        await wrapper.vm.$nextTick();
        // Both should be hidden now.
        const drawerAfter = wrapper.find('aside.project-details-drawer');
        expect(drawerAfter.classes()).not.toContain('open');
        expect(wrapper.findComponent({ name: 'BulkActionsBar' }).exists()).toBe(false);
        expect(wrapper.find('.projects-view').classes()).not.toContain('has-drawer');
    });

    it('singleSelectedProject = null when selected id is not in items', async () => {
        // Items contain only id=1, but we'll select id=999 (not present).
        mockProjects([projectA]);
        const wrapper = factory();
        await flushPromises();
        // Reach into store to add a phantom id directly (simulates stale selection
        // after items refetch dropped the project).
        const cards = wrapper.findAllComponents({ name: 'ProjectCard' });
        expect(cards.length).toBe(1);
        cards[0].vm.$emit('toggle-select', 999);
        await wrapper.vm.$nextTick();
        // Selection size is 1, but lookup fails → singleSelectedProject = null
        // → drawer NOT open.
        const drawer = wrapper.find('aside.project-details-drawer');
        expect(drawer.exists()).toBe(true);
        expect(drawer.classes()).not.toContain('open');
        // .has-drawer also depends on singleSelectedProject !== null.
        expect(wrapper.find('.projects-view').classes()).not.toContain('has-drawer');
    });
});

describe('ProjectsView 18:00 cutoff banner', () => {
    // Косяк 07: баннер «до 18:00» и фильтры показываются, только когда есть
    // проекты. Для банннер-тестов мокаем непустой аккаунт.
    const oneProject = {
        id: 1,
        name: 'A',
        signal_type: 'site',
        signal_identifier: 'a.ru',
        daily_limit_target: 10,
        delivered_today: 0,
        is_active: true,
        sync_status: 'ok',
    };
    beforeEach(() => {
        localStorage.clear();
        (axios.get as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
            data: { data: [oneProject], meta: { total: 1, current_page: 1, per_page: 20 } },
        });
    });

    it('shows the cutoff banner with the 18:00 deadline by default', async () => {
        const wrapper = factory();
        await flushPromises();
        const banner = wrapper.find('[data-testid="cutoff-banner"]');
        expect(banner.exists()).toBe(true);
        expect(banner.text()).toContain('18:00');
    });

    it('hides the banner after the close button and remembers it in localStorage', async () => {
        const wrapper = factory();
        await flushPromises();
        await wrapper.find('[data-testid="cutoff-banner-close"]').trigger('click');
        await wrapper.vm.$nextTick();
        expect(wrapper.find('[data-testid="cutoff-banner"]').exists()).toBe(false);
        expect(localStorage.getItem('projects.cutoffBannerDismissed')).toBe('1');
    });

    it('stays hidden on next mount when previously dismissed', async () => {
        localStorage.setItem('projects.cutoffBannerDismissed', '1');
        const wrapper = factory();
        await flushPromises();
        expect(wrapper.find('[data-testid="cutoff-banner"]').exists()).toBe(false);
    });
});

describe('ProjectsView — косяк 07: пустой аккаунт без шума', () => {
    beforeEach(() => {
        localStorage.clear();
        (axios.get as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
            data: { data: [], meta: { total: 0, current_page: 1, per_page: 20 } },
        });
    });

    it('при 0 проектов скрывает баннер «до 18:00», фильтры и пагинацию, оставляя подсказку', async () => {
        const wrapper = factory();
        await flushPromises();
        expect(wrapper.find('[data-testid="cutoff-banner"]').exists()).toBe(false);
        expect(wrapper.find('[data-testid="filter-region"]').exists()).toBe(false);
        expect(wrapper.find('[data-testid="perpage-toggle"]').exists()).toBe(false);
        expect(wrapper.text()).toMatch(/Нет проектов/i);
    });
});
