import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import React from 'react';

import { router, usePage } from '@inertiajs/react';

import LinkOpportunitiesIndex from './Index';

const { mockTrackProductEvent } = vi.hoisted(() => ({
    mockTrackProductEvent: vi.fn(),
}));
vi.mock('@/lib/analytics', () => ({
    trackProductEvent: mockTrackProductEvent,
}));

vi.mock('sonner', () => ({
    toast: {
        success: vi.fn(),
        error: vi.fn(),
    },
}));

// Mock ziggy-js the same way the page imports it (direct module import).
// setup.ts puts `route` on globalThis but the page does
// `import { route } from 'ziggy-js'`, which bypasses the global.
vi.mock('ziggy-js', () => ({
    route: vi.fn((name: string, params?: unknown) => {
        const siteId = Array.isArray(params) ? params[0] : params;
        const opportunityId = Array.isArray(params) ? params[1] : undefined;
        if (name === 'sites.link-opportunities.index')
            return `/sites/${siteId}/link-opportunities`;
        if (name === 'sites.link-opportunities.update')
            return `/sites/${siteId}/link-opportunities/${opportunityId}`;
        if (name === 'sites.link-opportunities.build')
            return `/sites/${siteId}/link-opportunities/build`;
        if (name === 'sites.link-opportunities.bulk-update')
            return `/sites/${siteId}/link-opportunities/bulk-update`;
        if (name === 'sites.link-opportunities.bulk-apply')
            return `/sites/${siteId}/link-opportunities/apply`;
        return '/mock-route';
    }),
}));

vi.mock('@inertiajs/react', async () => {
    const actual = await vi.importActual('@inertiajs/react');
    return {
        ...actual,
        usePage: vi.fn(),
        router: {
            get: vi.fn(),
            post: vi.fn(),
            patch: vi.fn(),
            reload: vi.fn(),
            on: vi.fn(() => () => {}),
        },
        Head: ({ title }: { title: string }) => <title>{title}</title>,
        Link: ({
            children,
            href,
        }: {
            children: React.ReactNode;
            href: string;
        }) => <a href={href}>{children}</a>,
    };
});

vi.mock('@/Layouts/DashboardLayout', () => ({
    default: ({ children }: { children: React.ReactNode }) => (
        <div data-testid="dashboard-layout">{children}</div>
    ),
}));

vi.mock('@/Components/Shared/InertiaPagination', () => ({
    default: () => <div data-testid="inertia-pagination" />,
}));

interface MockSiteProp {
    id: number;
    name: string;
    domain: string;
}

interface MockFiltersProp {
    status: string;
    min_relevance: number;
    search: string;
}

interface MockOpportunity {
    id: number;
    source: { id: number | null; title: string | null; url: string | null };
    target: { id: number | null; title: string | null; url: string | null };
    relevance_score: number;
    suggested_anchor: string;
    target_terms: Array<{ term: string; score: number }>;
    status: 'pending' | 'applied' | 'dismissed';
    applied_at: string | null;
    dismissed_at: string | null;
}

interface MockProps {
    site?: MockSiteProp;
    counts?: { pending: number; applied: number; dismissed: number };
    filters?: MockFiltersProp;
    data?: MockOpportunity[];
    error?: string | null;
}

const buildProps = (overrides: MockProps = {}) => {
    const defaults = {
        site: { id: 42, name: 'Test Site', domain: 'example.com' },
        counts: { pending: 3, applied: 1, dismissed: 0 },
        filters: { status: 'pending', min_relevance: 0, search: '' },
        data: [
            {
                id: 1,
                source: {
                    id: 100,
                    title: 'Source Article About React',
                    url: 'https://example.com/source-react',
                },
                target: {
                    id: 200,
                    title: 'Target Article About Hooks',
                    url: 'https://example.com/target-hooks',
                },
                relevance_score: 0.82,
                suggested_anchor: 'React hooks',
                target_terms: [{ term: 'react', score: 0.9 }],
                status: 'pending' as const,
                applied_at: null,
                dismissed_at: null,
            },
        ] as MockOpportunity[],
        error: null,
    };

    const merged = { ...defaults, ...overrides };

    return {
        site: merged.site,
        opportunities: {
            data: merged.data,
            links: [],
            current_page: 1,
            last_page: 1,
            total: merged.data.length,
        },
        counts: merged.counts,
        filters: merged.filters,
        error: merged.error,
        // Inertia shared-prop noise so tests don't crash if the component
        // happens to destructure something extra in future.
        auth: {
            user: { name: 'Test User', email: 'test@example.com' },
        },
        features: { notifications: false },
    };
};

const mockedUsePage = vi.mocked(usePage);
const mockedRouter = vi.mocked(router);

const setupPage = (overrides: MockProps = {}) => {
    mockedUsePage.mockReturnValue({
        props: buildProps(overrides),
        url: `/sites/${overrides.site?.id ?? 42}/link-opportunities`,
        component: 'Sites/LinkOpportunities/Index',
        version: '1',
    } as unknown as ReturnType<typeof usePage>);
};

beforeEach(() => {
    vi.clearAllMocks();
    vi.stubGlobal(
        'route',
        vi.fn((name: string, params?: unknown) => {
            const siteId = Array.isArray(params) ? params[0] : params;
            const opportunityId = Array.isArray(params) ? params[1] : undefined;
            if (name === 'sites.link-opportunities.index')
                return `/sites/${siteId}/link-opportunities`;
            if (name === 'sites.link-opportunities.update')
                return `/sites/${siteId}/link-opportunities/${opportunityId}`;
            if (name === 'sites.link-opportunities.build')
                return `/sites/${siteId}/link-opportunities/build`;
            if (name === 'sites.link-opportunities.bulk-update')
                return `/sites/${siteId}/link-opportunities/bulk-update`;
            if (name === 'sites.link-opportunities.bulk-apply')
                return `/sites/${siteId}/link-opportunities/apply`;
            return '/mock-route';
        }),
    );
});

describe('LinkOpportunities/Index', () => {
    describe('page header', () => {
        it('renders the editorial title and subtitle from props', () => {
            setupPage();
            render(<LinkOpportunitiesIndex />);

            expect(
                screen.getByRole('heading', {
                    name: 'Internal link opportunities',
                }),
            ).toBeInTheDocument();
            expect(
                screen.getByText(/Targeted link suggestions across pages on example\.com/),
            ).toBeInTheDocument();
        });

        it('renders the Pending and Applied readouts with counts', () => {
            setupPage({ counts: { pending: 7, applied: 4, dismissed: 2 } });
            render(<LinkOpportunitiesIndex />);

            // The readouts render their label next to the value. We check both
            // labels exist and the underlying counts are present.
            expect(screen.getByText('Pending')).toBeInTheDocument();
            expect(screen.getByText('Applied')).toBeInTheDocument();
            expect(screen.getByText('7')).toBeInTheDocument();
            expect(screen.getByText('4')).toBeInTheDocument();
        });

        it('renders a Re-run analysis action button in the header', () => {
            setupPage();
            render(<LinkOpportunitiesIndex />);

            expect(
                screen.getByRole('button', { name: /Re-run analysis/i }),
            ).toBeInTheDocument();
        });
    });

    describe('analytics', () => {
        it('fires LINK_OPPORTUNITIES_VIEWED on mount with site_id', () => {
            setupPage({ site: { id: 99, name: 'Other', domain: 'other.test' } });
            render(<LinkOpportunitiesIndex />);

            expect(mockTrackProductEvent).toHaveBeenCalledWith(
                'link_opportunities_viewed',
                { site_id: '99' },
            );
        });
    });

    describe('filters', () => {
        it('renders the relevance slider with a percent-formatted label', () => {
            setupPage({
                filters: { status: 'pending', min_relevance: 0.25, search: '' },
            });
            render(<LinkOpportunitiesIndex />);

            // The slider label is "Min relevance" and the live value renders
            // as a percent string ("25%"). 0.25 fraction → 25%.
            expect(screen.getAllByText(/Min relevance/i).length).toBeGreaterThan(
                0,
            );
            // Use getAllBy — percent label appears in both desktop and mobile
            // Sheet copies of the bar. Either presence is sufficient.
            expect(screen.getAllByText('25%').length).toBeGreaterThan(0);
        });

        it('routes to a default-filtered URL when Clear all filters is clicked from the filtered-empty state', async () => {
            const user = userEvent.setup();
            // Pending count present but visible data empty AND a search filter
            // active → filtered empty state with the Clear-all CTA.
            setupPage({
                counts: { pending: 3, applied: 1, dismissed: 0 },
                filters: { status: 'pending', min_relevance: 0, search: 'rare' },
                data: [],
            });
            render(<LinkOpportunitiesIndex />);

            const clearBtn = screen.getByRole('button', {
                name: /Clear all filters/i,
            });
            await user.click(clearBtn);

            expect(mockedRouter.get).toHaveBeenCalledWith(
                '/sites/42/link-opportunities',
                { status: 'pending' },
                expect.objectContaining({
                    preserveState: true,
                    preserveScroll: true,
                    replace: true,
                }),
            );
        });
    });

    describe('empty states', () => {
        it('renders the zero-state when no opportunities exist for any status', () => {
            setupPage({
                counts: { pending: 0, applied: 0, dismissed: 0 },
                data: [],
            });
            render(<LinkOpportunitiesIndex />);

            expect(
                screen.getByText(/No internal link opportunities yet/),
            ).toBeInTheDocument();
        });

        it('renders the filtered-empty state when totals exist but filtered data is empty', () => {
            setupPage({
                counts: { pending: 5, applied: 2, dismissed: 0 },
                filters: { status: 'pending', min_relevance: 0.9, search: '' },
                data: [],
            });
            render(<LinkOpportunitiesIndex />);

            expect(
                screen.getByText(/No opportunities match your filters/),
            ).toBeInTheDocument();
            expect(
                screen.getByRole('button', { name: /Clear all filters/i }),
            ).toBeInTheDocument();
        });

        it('renders the error empty-state and a retry button when error prop is set', async () => {
            const user = userEvent.setup();
            setupPage({
                error: 'We couldn\'t load link opportunities right now. Please try again.',
                data: [],
            });
            render(<LinkOpportunitiesIndex />);

            expect(
                screen.getByText(/Couldn't load opportunities/),
            ).toBeInTheDocument();
            const retry = screen.getByRole('button', { name: /Try again/i });
            await user.click(retry);
            expect(mockedRouter.reload).toHaveBeenCalled();
        });
    });

    describe('table', () => {
        it('renders opportunity rows with source, target, anchor and relevance', () => {
            setupPage();
            render(<LinkOpportunitiesIndex />);

            expect(
                screen.getByText('Source Article About React'),
            ).toBeInTheDocument();
            expect(
                screen.getByText('Target Article About Hooks'),
            ).toBeInTheDocument();
            expect(screen.getByText('React hooks')).toBeInTheDocument();
            // Relevance badge: 0.82 → 82%.
            expect(screen.getByText('82%')).toBeInTheDocument();
        });

        it('renders Mark as added and Dismiss buttons for pending rows', () => {
            setupPage();
            render(<LinkOpportunitiesIndex />);

            // Renamed from "Mark applied" to "Mark as added" (R3UXS-018)
            expect(
                screen.getByRole('button', { name: /Mark as added/i }),
            ).toBeInTheDocument();
            expect(
                screen.getByRole('button', { name: /^Dismiss$/i }),
            ).toBeInTheDocument();
        });
    });

    // R10FE-102: per-row RowStatusButton — per-row isolation + 422 error surfacing
    describe('per-row status buttons (R10FE-102)', () => {
        it('surfaces a toast error when a row-level patch returns a validation error (422)', async () => {
            const { toast } = await import('sonner');

            // Capture the opts Inertia passes so we can invoke onError manually.
            // Cast via ReturnType<typeof vi.fn> to bypass Inertia's overloaded url
            // signature (same pattern as SiteComparisonTable.test.tsx).
            let capturedOpts: {
                onError?: (errors: Record<string, string>) => void;
                onFinish?: () => void;
            } = {};
            (mockedRouter.patch as ReturnType<typeof vi.fn>).mockImplementation(
                (_url: unknown, _data: unknown, opts: typeof capturedOpts) => {
                    capturedOpts = opts;
                },
            );

            setupPage();
            render(<LinkOpportunitiesIndex />);

            const markBtn = screen.getByRole('button', { name: /Mark as added/i });
            act(() => { markBtn.click(); });

            // Trigger a 422-like validation error from the server.
            act(() => {
                capturedOpts.onError?.({ status: 'The status field is invalid.' });
                capturedOpts.onFinish?.();
            });

            expect(toast.error).toHaveBeenCalled();
        });

        it('clicking row A button does not disable row B buttons while row A is in flight', async () => {
            // Two-row setup: row A (pending, id=1) + row B (pending, id=2).
            const twoRows = [
                {
                    id: 1,
                    source: { id: 10, title: 'Row A Source', url: 'https://a.test/src' },
                    target: { id: 11, title: 'Row A Target', url: 'https://a.test/tgt' },
                    relevance_score: 0.9,
                    suggested_anchor: 'anchor a',
                    target_terms: [],
                    status: 'pending' as const,
                    applied_at: null,
                    dismissed_at: null,
                },
                {
                    id: 2,
                    source: { id: 20, title: 'Row B Source', url: 'https://b.test/src' },
                    target: { id: 21, title: 'Row B Target', url: 'https://b.test/tgt' },
                    relevance_score: 0.8,
                    suggested_anchor: 'anchor b',
                    target_terms: [],
                    status: 'pending' as const,
                    applied_at: null,
                    dismissed_at: null,
                },
            ];

            // Capture calls per-row so we can control settling independently.
            // Cast via ReturnType<typeof vi.fn> to bypass Inertia's overloaded url
            // signature (same pattern as SiteComparisonTable.test.tsx).
            const capturedByRow: Array<{ onFinish?: () => void }> = [];
            (mockedRouter.patch as ReturnType<typeof vi.fn>).mockImplementation(
                (_url: unknown, _data: unknown, opts: { onFinish?: () => void }) => {
                    capturedByRow.push(opts);
                },
            );

            setupPage({ data: twoRows, counts: { pending: 2, applied: 0, dismissed: 0 } });
            render(<LinkOpportunitiesIndex />);

            // Both rows render "Mark as added" and "Dismiss" buttons.
            const markBtns = screen.getAllByRole('button', { name: /Mark as added/i });
            expect(markBtns).toHaveLength(2);

            // Click row A's "Mark as added" — puts row A in flight.
            act(() => { markBtns[0].click(); });

            // Row A's button is now loading/disabled; Row B's button must remain enabled.
            const markBtnsAfter = screen.getAllByRole('button', { name: /Mark as added/i });
            expect(markBtnsAfter[0]).toBeDisabled();
            expect(markBtnsAfter[1]).not.toBeDisabled();

            // Settle row A.
            act(() => { capturedByRow[0]?.onFinish?.(); });

            // After settling, row A re-enables.
            const markBtnsFinal = screen.getAllByRole('button', { name: /Mark as added/i });
            expect(markBtnsFinal[0]).not.toBeDisabled();
        });

        it('within-row mutual exclusion: clicking "Mark as added" disables sibling "Dismiss" on same row while in flight', async () => {
            // Capture opts so we can control when the in-flight PATCH settles.
            let capturedOpts: { onFinish?: () => void } = {};
            (mockedRouter.patch as ReturnType<typeof vi.fn>).mockImplementation(
                (_url: unknown, _data: unknown, opts: { onFinish?: () => void }) => {
                    capturedOpts = opts;
                },
            );

            setupPage();
            render(<LinkOpportunitiesIndex />);

            const markBtn = screen.getByRole('button', { name: /Mark as added/i });
            const dismissBtn = screen.getByRole('button', { name: /Dismiss/i });

            // Neither button is disabled initially.
            expect(markBtn).not.toBeDisabled();
            expect(dismissBtn).not.toBeDisabled();

            // Click "Mark as added" — puts the row in flight.
            act(() => { markBtn.click(); });

            // "Mark as added" is now loading/disabled and "Dismiss" must be locked too.
            expect(screen.getByRole('button', { name: /Mark as added/i })).toBeDisabled();
            expect(screen.getByRole('button', { name: /Dismiss/i })).toBeDisabled();

            // Settle the in-flight PATCH — both buttons re-enable.
            act(() => { capturedOpts.onFinish?.(); });

            expect(screen.getByRole('button', { name: /Mark as added/i })).not.toBeDisabled();
            expect(screen.getByRole('button', { name: /Dismiss/i })).not.toBeDisabled();
        });
    });

    // R6PROD-007: bulk-apply executor
    describe('bulk-apply toolbar', () => {
        it('shows the Insert into WordPress button in the bulk toolbar after selecting a row', async () => {
            const user = userEvent.setup();
            setupPage();
            render(<LinkOpportunitiesIndex />);

            // Select the pending row via its checkbox (R6UXS-013: now a shadcn Checkbox, role=checkbox)
            const selectBtn = screen.getByRole('checkbox', { name: /Select row/i });
            await user.click(selectBtn);

            // The bulk toolbar should now be visible with the Insert into WordPress CTA
            expect(
                screen.getAllByRole('button', { name: /Insert into WordPress/i }).length,
            ).toBeGreaterThan(0);
        });

        it('calls the bulk-apply endpoint when Insert into WordPress is clicked', async () => {
            const user = userEvent.setup();
            setupPage();
            render(<LinkOpportunitiesIndex />);

            // R6UXS-013: shadcn Checkbox renders as role=checkbox, not role=button
            const selectBtn = screen.getByRole('checkbox', { name: /Select row/i });
            await user.click(selectBtn);

            const applyBtn = screen.getAllByRole('button', {
                name: /Insert into WordPress/i,
            })[0];
            await user.click(applyBtn);

            expect(mockedRouter.post).toHaveBeenCalledWith(
                '/sites/42/link-opportunities/apply',
                expect.objectContaining({ ids: expect.arrayContaining([1]) }),
                expect.objectContaining({ preserveScroll: true }),
            );
        });
    });
});
