import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, cleanup } from '@testing-library/svelte';
import { mockQuery } from '../../test-utils/query';

// Mutable mock-user handle so individual tests can flip is_admin / null
// without re-importing the module under test.
const userState = vi.hoisted(() => ({
  current: null as { id: string; username: string; is_admin: boolean } | null
}));

vi.mock('$lib/auth/store.svelte', () => ({
  user: { get value() { return userState.current; } }
}));

vi.mock('$app/state', () => ({
  page: { url: new URL('http://localhost/admin') }
}));

vi.mock('$lib/api/admin', async () => {
  const { readable } = await import('svelte/store');
  return {
    createAdminRequestsQuery: vi.fn(),
    createLidarrConfigQuery: vi.fn(),
    createAdminQuarantineQuery: vi.fn(),
    createScanStatusQuery: vi.fn(() =>
      readable({
        data: { id: '', started_at: '', finished_at: null, in_flight: false },
        isPending: false,
        isError: false
      })
    ),
    createCoverageQuery: () =>
      readable({
        data: undefined,
        isPending: false,
        isError: false
      }),
    approveRequest: vi.fn().mockResolvedValue({}),
    rejectRequest: vi.fn().mockResolvedValue({}),
    resolveQuarantine: vi.fn().mockResolvedValue({}),
    deleteQuarantineFile: vi.fn().mockResolvedValue({}),
    deleteQuarantineViaLidarr: vi.fn().mockResolvedValue({}),
    triggerScan: vi.fn().mockResolvedValue({}),
    refetchMissingCovers: vi.fn().mockResolvedValue({ started: true }),
    researchMissingArt: vi.fn().mockResolvedValue({ version: 1 })
  };
});

afterEach(() => {
  cleanup();
  vi.clearAllMocks();
  userState.current = null;
});

describe('admin/+layout.ts load gate', () => {
  test('non-admin user throws redirect to /', async () => {
    userState.current = { id: 'u1', username: 'alice', is_admin: false };
    const { load } = await import('./+layout');
    await expect(async () =>
      load({} as Parameters<typeof load>[0])
    ).rejects.toMatchObject({ status: 302, location: '/' });
  });

  test('unauthenticated (null user) throws redirect to /', async () => {
    userState.current = null;
    const { load } = await import('./+layout');
    await expect(async () =>
      load({} as Parameters<typeof load>[0])
    ).rejects.toMatchObject({ status: 302, location: '/' });
  });

  test('admin user passes the gate', async () => {
    userState.current = { id: 'u1', username: 'root', is_admin: true };
    const { load } = await import('./+layout');
    expect(load({} as Parameters<typeof load>[0])).toEqual({});
  });
});

// Top-level imports — Svelte/Vite caches the module graph across tests,
// and dynamic re-imports inside each test produced "stale-mock" symptoms
// where the page picked up empty data despite `mockReturnValue` being set.
// Importing at module load means the mocks attach to the same handles the
// page binds against.
import {
  createAdminRequestsQuery,
  createLidarrConfigQuery,
  createAdminQuarantineQuery,
  createScanStatusQuery,
  approveRequest,
  rejectRequest,
  resolveQuarantine,
  deleteQuarantineFile,
  deleteQuarantineViaLidarr,
  researchMissingArt
} from '$lib/api/admin';
import OverviewPage from './+page.svelte';

function setupOverview(opts: {
  requests?: unknown[];
  quarantine?: unknown[];
  lidarr?: { enabled: boolean };
} = {}) {
  (createAdminRequestsQuery as ReturnType<typeof vi.fn>).mockReturnValue(
    mockQuery({ data: opts.requests ?? [] })
  );
  (createAdminQuarantineQuery as ReturnType<typeof vi.fn>).mockReturnValue(
    mockQuery({ data: opts.quarantine ?? [] })
  );
  (createLidarrConfigQuery as ReturnType<typeof vi.fn>).mockReturnValue(
    mockQuery({ data: opts.lidarr ?? { enabled: false } })
  );
}

describe('admin Overview page', () => {
  test('renders top stat cards (requests, quarantine, lidarr)', async () => {
    setupOverview({
      requests: [{ id: '1' }, { id: '2' }],
      quarantine: [{ track_id: 't1', report_count: 2, reports: [] }],
      lidarr: { enabled: true }
    });
    render(OverviewPage);
    // "Pending requests" appears as both stat-card label and section
    // header — assert via the link role to disambiguate to the stat card.
    const reqLink = screen.getByRole('link', { name: /pending requests/i });
    expect(reqLink).toHaveTextContent('2');
    const quarLink = screen.getByRole('link', { name: /quarantined tracks/i });
    expect(quarLink).toHaveTextContent('1');
    expect(screen.getByText(/connected/i)).toBeInTheDocument();
  });

  test('shows empty-state copy when both queues are empty', async () => {
    setupOverview({
      requests: [],
      quarantine: [],
      lidarr: { enabled: false }
    });
    render(OverviewPage);
    expect(screen.getByText(/no pending requests/i)).toBeInTheDocument();
    expect(screen.getByText(/no quarantined tracks/i)).toBeInTheDocument();
    expect(screen.getByText(/unset/i)).toBeInTheDocument();
  });

  test('top stat cards link to their sub-pages', async () => {
    setupOverview();
    render(OverviewPage);
    expect(screen.getByRole('link', { name: /pending requests/i }))
      .toHaveAttribute('href', '/admin/requests');
    expect(screen.getByRole('link', { name: /quarantined tracks/i }))
      .toHaveAttribute('href', '/admin/quarantine');
    expect(screen.getByRole('link', { name: /^lidarr/i }))
      .toHaveAttribute('href', '/admin/integrations');
  });

  test('clicking Approve on a request row calls approveRequest', async () => {
    setupOverview({
      requests: [
        { id: 'r1', kind: 'artist', artist_name: 'Boards of Canada', status: 'pending' }
      ]
    });
    render(OverviewPage);
    await fireEvent.click(screen.getByRole('button', { name: /approve boards of canada/i }));
    expect(approveRequest).toHaveBeenCalledWith('r1');
  });

  test('clicking Reject on a request row calls rejectRequest', async () => {
    setupOverview({
      requests: [
        { id: 'r1', kind: 'artist', artist_name: 'Boards of Canada', status: 'pending' }
      ]
    });
    render(OverviewPage);
    await fireEvent.click(screen.getByRole('button', { name: /reject boards of canada/i }));
    expect(rejectRequest).toHaveBeenCalledWith('r1');
  });

  test('Resolve fires immediately on a quarantine row', async () => {
    setupOverview({
      quarantine: [
        { track_id: 't1', track_title: 'Roygbiv', artist_name: 'BoC',
          album_title: 'Geogaddi', report_count: 1, reports: [] }
      ]
    });
    render(OverviewPage);
    await fireEvent.click(screen.getByRole('button', { name: /resolve roygbiv/i }));
    expect(resolveQuarantine).toHaveBeenCalledWith('t1');
  });

  test('Delete file requires two clicks (inline confirm)', async () => {
    setupOverview({
      quarantine: [
        { track_id: 't1', track_title: 'Roygbiv', artist_name: 'BoC',
          album_title: 'Geogaddi', report_count: 1, reports: [] }
      ]
    });
    render(OverviewPage);
    // First click: arms the confirm; no API call yet.
    await fireEvent.click(screen.getByRole('button', { name: /^delete file roygbiv$/i }));
    expect(deleteQuarantineFile).not.toHaveBeenCalled();
    // Second click on the now-armed button fires the action.
    await fireEvent.click(
      screen.getByRole('button', { name: /confirm delete file roygbiv/i })
    );
    expect(deleteQuarantineFile).toHaveBeenCalledWith('t1');
  });

  test('Delete via Lidarr requires two clicks (inline confirm)', async () => {
    setupOverview({
      quarantine: [
        { track_id: 't1', track_title: 'Roygbiv', artist_name: 'BoC',
          album_title: 'Geogaddi', report_count: 1, reports: [] }
      ]
    });
    render(OverviewPage);
    await fireEvent.click(
      screen.getByRole('button', { name: /^delete via lidarr roygbiv$/i })
    );
    expect(deleteQuarantineViaLidarr).not.toHaveBeenCalled();
    await fireEvent.click(
      screen.getByRole('button', { name: /confirm delete via lidarr roygbiv/i })
    );
    expect(deleteQuarantineViaLidarr).toHaveBeenCalledWith('t1');
  });

  test('"View all" link surfaces only when count exceeds the preview limit', async () => {
    // 6 requests > limit of 5; quarantine empty.
    const six = Array.from({ length: 6 }, (_, i) => ({
      id: `r${i}`,
      kind: 'artist' as const,
      artist_name: `Artist ${i}`,
      status: 'pending' as const
    }));
    setupOverview({ requests: six });
    render(OverviewPage);
    expect(screen.getByRole('link', { name: /view all 6/i })).toHaveAttribute(
      'href', '/admin/requests'
    );
  });

  test('renders Re-search missing art button and invokes on click', async () => {
    setupOverview();
    render(OverviewPage);
    const btn = screen.getByRole('button', { name: /re-search missing art/i });
    expect(btn).toBeInTheDocument();
    await fireEvent.click(btn);
    expect(researchMissingArt).toHaveBeenCalled();
  });

  test('Library scan card shows Processing badge when library tally is populated and in-flight', async () => {
    const { readable } = await import('svelte/store');
    (createScanStatusQuery as ReturnType<typeof vi.fn>).mockReturnValueOnce(
      readable({
        data: {
          id: 'in-flight',
          started_at: '2026-05-06T11:00:00Z',
          finished_at: null,
          in_flight: true,
          library: { scanned: 100, added: 1, updated: 0, skipped: 99, errored: 0 }
        },
        isPending: false,
        isError: false
      })
    );
    setupOverview();
    userState.current = { id: 'admin', username: 'admin', is_admin: true };
    render(OverviewPage);
    expect(await screen.findByText('Processing')).toBeInTheDocument();
  });
});
