import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor, within } from '@testing-library/svelte';
import { mockQuery } from '../../../test-utils/query';
import type { LidarrConfig } from '$lib/api/types';
import type { CoverProvider, TagProvider, SMTPConfig } from '$lib/api/admin';

// useQueryClient is wrapped so the page can call invalidateQueries() without
// a real QueryClient context. Per-file mock OVERRIDES the vitest.setup default
// so assertions see this named spy rather than the setup's anonymous fn.
const invalidateQueries = vi.fn();
vi.mock('@tanstack/svelte-query', async (orig) => {
  const actual = (await orig()) as Record<string, unknown>;
  return { ...actual, useQueryClient: () => ({ invalidateQueries }) };
});

vi.mock('$lib/api/admin', () => ({
  createLidarrConfigQuery: vi.fn(),
  createQualityProfilesQuery: vi.fn(),
  createMetadataProfilesQuery: vi.fn(),
  createRootFoldersQuery: vi.fn(),
  putLidarrConfig: vi.fn(),
  testLidarrConnection: vi.fn(),
  createCoverProvidersQuery: vi.fn(),
  updateCoverProvider: vi.fn(),
  testCoverProvider: vi.fn(),
  createTagProvidersQuery: vi.fn(),
  updateTagProvider: vi.fn(),
  testTagProvider: vi.fn(),
  createSMTPConfigQuery: vi.fn(),
  updateSMTPConfig: vi.fn(),
  testSMTPConfig: vi.fn()
}));

import IntegrationsPage from './+page.svelte';
import {
  createLidarrConfigQuery,
  createQualityProfilesQuery,
  createMetadataProfilesQuery,
  createRootFoldersQuery,
  putLidarrConfig,
  testLidarrConnection,
  createCoverProvidersQuery,
  updateCoverProvider,
  testCoverProvider,
  createTagProvidersQuery,
  updateTagProvider,
  testTagProvider,
  createSMTPConfigQuery,
  updateSMTPConfig,
  testSMTPConfig
} from '$lib/api/admin';

const cfgConnected: LidarrConfig = {
  enabled: true,
  base_url: 'http://lidarr.local',
  api_key: '***',
  default_quality_profile_id: 1,
  default_metadata_profile_id: 1,
  default_root_folder_path: '/music'
};

const cfgUnset: LidarrConfig = {
  enabled: false,
  base_url: '',
  api_key: '',
  default_quality_profile_id: 0,
  default_metadata_profile_id: 0,
  default_root_folder_path: ''
};

// Fixture cover providers
const providerMbcaa: CoverProvider = {
  id: 'mbcaa',
  display_name: 'MusicBrainz CAA',
  requires_api_key: false,
  supports: ['album_cover'],
  enabled: true,
  api_key_set: false,
  display_order: 0,
  testable: true
};

const providerTheAudioDB: CoverProvider = {
  id: 'theaudiodb',
  display_name: 'TheAudioDB',
  requires_api_key: true,
  supports: ['album_cover', 'artist_thumb', 'artist_fanart'],
  enabled: true,
  api_key_set: false,
  display_order: 1,
  testable: true
};

const providerTheAudioDBKeySet: CoverProvider = {
  ...providerTheAudioDB,
  api_key_set: true
};

const defaultCoverProviders = {
  providers: [providerMbcaa, providerTheAudioDB],
  sources_version: 1
};

// Fixture tag providers
const providerMusicbrainz: TagProvider = {
  id: 'musicbrainz',
  display_name: 'MusicBrainz',
  requires_api_key: false,
  supports: ['track_tags'],
  enabled: true,
  api_key_set: false,
  display_order: 0,
  testable: true
};

const providerLastfm: TagProvider = {
  id: 'lastfm',
  display_name: 'Last.fm',
  requires_api_key: true,
  supports: ['track_tags'],
  enabled: false,
  api_key_set: false,
  display_order: 1,
  testable: true
};

const defaultTagProviders = {
  providers: [providerMusicbrainz, providerLastfm],
  sources_version: 1
};

const testResponseOk = {
  ok: true as const,
  version: '2.0.5',
  quality_profiles: [
    { id: 1, name: 'FLAC' },
    { id: 2, name: 'MP3-320' }
  ],
  metadata_profiles: [{ id: 1, name: 'Standard' }],
  root_folders: [{ path: '/music', accessible: true, free_space: 12345 }]
};

const smtpUnset: SMTPConfig = {
  enabled: false,
  host: '',
  port: 587,
  username: '',
  password: '',
  from_address: '',
  from_name: 'Minstrel',
  use_tls: true
};

const smtpConfigured: SMTPConfig = {
  enabled: true,
  host: 'smtp.example.com',
  port: 587,
  username: 'user@example.com',
  password: '***',
  from_address: 'noreply@example.com',
  from_name: 'Minstrel',
  use_tls: true
};

afterEach(() => {
  vi.clearAllMocks();
  invalidateQueries.mockReset();
});

function setup(opts: { config?: LidarrConfig; coverProviders?: typeof defaultCoverProviders | null; tagProviders?: typeof defaultTagProviders | null; smtp?: SMTPConfig } = {}) {
  (createLidarrConfigQuery as ReturnType<typeof vi.fn>).mockReturnValue(
    mockQuery({ data: opts.config ?? cfgConnected })
  );
  (createQualityProfilesQuery as ReturnType<typeof vi.fn>).mockReturnValue(
    mockQuery({
      data: [
        { id: 1, name: 'Standard' },
        { id: 2, name: 'Lossless' }
      ]
    })
  );
  (createMetadataProfilesQuery as ReturnType<typeof vi.fn>).mockReturnValue(
    mockQuery({
      data: [
        { id: 1, name: 'Standard' },
        { id: 2, name: 'Singles only' }
      ]
    })
  );
  (createRootFoldersQuery as ReturnType<typeof vi.fn>).mockReturnValue(
    mockQuery({ data: [{ path: '/music', accessible: true, free_space: 0 }] })
  );
  const cpData = opts.coverProviders === null ? undefined : (opts.coverProviders ?? defaultCoverProviders);
  (createCoverProvidersQuery as ReturnType<typeof vi.fn>).mockReturnValue(
    mockQuery({ data: cpData, isPending: opts.coverProviders === null })
  );
  const tpData = opts.tagProviders === null ? undefined : (opts.tagProviders ?? defaultTagProviders);
  (createTagProvidersQuery as ReturnType<typeof vi.fn>).mockReturnValue(
    mockQuery({ data: tpData, isPending: opts.tagProviders === null })
  );
  (createSMTPConfigQuery as ReturnType<typeof vi.fn>).mockReturnValue(
    mockQuery({ data: opts.smtp ?? smtpUnset })
  );
  return render(IntegrationsPage);
}

// Helper for finding the modal's Disconnect confirm button. Both the panel
// trigger and the modal confirm share the visible name "Disconnect"; the
// modal one is the descendant of role="dialog".
function modalConfirmButton(): HTMLButtonElement {
  const matches = screen.getAllByRole('button', { name: /^disconnect$/i });
  const inDialog = matches.find((b) => b.closest('[role="dialog"]') !== null);
  if (!inDialog) throw new Error('modal Disconnect button not found');
  return inDialog as HTMLButtonElement;
}

// Section-scoping helpers. Both Lidarr and the cover-art-providers panel render
// "Save changes", "Test connection", and "API key" controls, so unscoped queries
// match multiple elements. These helpers narrow every query to its own <section>.

function lidarrSection(): HTMLElement {
  return screen.getByRole('heading', { name: /^lidarr$/i, level: 3 }).closest('section') as HTMLElement;
}

function coverProvidersSection(): HTMLElement {
  return screen.getByRole('heading', { name: /cover art providers/i, level: 3 }).closest('section') as HTMLElement;
}

// Each provider is rendered inside an <article>. Provider headings are h4.
function providerCard(providerName: string): HTMLElement {
  return within(coverProvidersSection())
    .getByRole('heading', { name: providerName, level: 4 })
    .closest('article') as HTMLElement;
}

function tagProvidersSection(): HTMLElement {
  return screen.getByRole('heading', { name: /tag enrichment sources/i, level: 3 }).closest('section') as HTMLElement;
}

function tagProviderCard(providerName: string): HTMLElement {
  return within(tagProvidersSection())
    .getByRole('heading', { name: providerName, level: 4 })
    .closest('article') as HTMLElement;
}

describe('/admin/integrations', () => {
  test('Save calls putLidarrConfig with empty api_key when input is blank', async () => {
    setup();
    // Save now runs Test first (commit bca8622); mock both endpoints. The
    // test response also has to match enough of the existing config that
    // the Save handler treats it as a valid passthrough.
    (testLidarrConnection as ReturnType<typeof vi.fn>).mockResolvedValueOnce(testResponseOk);
    (putLidarrConfig as ReturnType<typeof vi.fn>).mockResolvedValueOnce(cfgConnected);
    await fireEvent.click(within(lidarrSection()).getByRole('button', { name: /save changes/i }));
    await waitFor(() => {
      expect(putLidarrConfig).toHaveBeenCalledWith(
        expect.objectContaining({
          enabled: true,
          base_url: 'http://lidarr.local',
          api_key: '',
          default_quality_profile_id: 1,
          default_root_folder_path: '/music'
        })
      );
    });
  });

  test('Save with new api_key sends the typed value', async () => {
    setup();
    const apiInput = screen.getByPlaceholderText(/saved — leave empty to keep/i);
    await fireEvent.input(apiInput, { target: { value: 'newkey' } });
    (testLidarrConnection as ReturnType<typeof vi.fn>).mockResolvedValueOnce(testResponseOk);
    (putLidarrConfig as ReturnType<typeof vi.fn>).mockResolvedValueOnce(cfgConnected);
    await fireEvent.click(within(lidarrSection()).getByRole('button', { name: /save changes/i }));
    await waitFor(() => {
      expect(putLidarrConfig).toHaveBeenCalledWith(
        expect.objectContaining({ api_key: 'newkey' })
      );
    });
  });

  test('Test connection renders Lidarr version on success', async () => {
    setup();
    (testLidarrConnection as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
      ok: true,
      version: '2.0.0'
    });
    await fireEvent.click(
      within(lidarrSection()).getByRole('button', { name: /test connection/i })
    );
    await waitFor(() =>
      expect(screen.getByText(/lidarr 2\.0\.0/i)).toBeInTheDocument()
    );
  });

  test('Test connection renders error code on failure', async () => {
    setup();
    (testLidarrConnection as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
      ok: false,
      error: 'lidarr_unreachable'
    });
    await fireEvent.click(
      within(lidarrSection()).getByRole('button', { name: /test connection/i })
    );
    await waitFor(() =>
      expect(screen.getByText(/lidarr_unreachable/i)).toBeInTheDocument()
    );
  });

  test('Disconnect requires typed confirm; cancelling does not call API', async () => {
    setup();
    // Open modal via the panel-level Disconnect button.
    await fireEvent.click(
      screen.getByRole('button', { name: /^disconnect$/i })
    );
    expect(screen.getByRole('dialog')).toBeInTheDocument();
    // Confirm button is disabled until "DISCONNECT" is typed.
    expect(modalConfirmButton().disabled).toBe(true);
    await fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
    expect(putLidarrConfig).not.toHaveBeenCalled();
    expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
  });

  test('Disconnect with typed confirmation calls API with cleared config', async () => {
    setup();
    await fireEvent.click(
      screen.getByRole('button', { name: /^disconnect$/i })
    );
    const confirmInput = screen.getByPlaceholderText('DISCONNECT');
    await fireEvent.input(confirmInput, { target: { value: 'DISCONNECT' } });
    (putLidarrConfig as ReturnType<typeof vi.fn>).mockResolvedValueOnce(cfgUnset);
    await fireEvent.click(modalConfirmButton());
    expect(putLidarrConfig).toHaveBeenCalledWith(
      expect.objectContaining({
        enabled: false,
        base_url: '',
        api_key: '',
        default_quality_profile_id: 0,
        default_root_folder_path: ''
      })
    );
  });

  test('Quality / metadata / root folder dropdowns populate from API', () => {
    setup();
    // 'Standard' appears in both quality + metadata fixtures, so use *AllBy*.
    expect(screen.getAllByText('Standard').length).toBeGreaterThanOrEqual(2);
    expect(screen.getByText('Lossless')).toBeInTheDocument();
    expect(screen.getByText('Singles only')).toBeInTheDocument();
    expect(screen.getByText('/music')).toBeInTheDocument();
  });

  test('header shows "Lidarr · connected" when configured', () => {
    setup();
    expect(screen.getByText(/lidarr · connected/i)).toBeInTheDocument();
  });

  test('header shows "Lidarr · unset" when not configured', () => {
    setup({ config: cfgUnset });
    expect(screen.getByText(/lidarr · unset/i)).toBeInTheDocument();
  });
});

describe('/admin/integrations — cover art providers', () => {
  test('renders one card per provider in the response', () => {
    setup();
    expect(screen.getByText('MusicBrainz CAA')).toBeInTheDocument();
    expect(screen.getByText('TheAudioDB')).toBeInTheDocument();
  });

  test('capability badges render for each capability in supports[]', () => {
    setup();
    // TheAudioDB has album_cover, artist_thumb, artist_fanart
    expect(screen.getAllByText('album cover').length).toBeGreaterThanOrEqual(1);
    expect(screen.getByText('artist thumb')).toBeInTheDocument();
    expect(screen.getByText('artist fanart')).toBeInTheDocument();
  });

  test('"✓ Set" indicator renders when api_key_set is true', () => {
    setup({
      coverProviders: {
        providers: [providerTheAudioDBKeySet],
        sources_version: 1
      }
    });
    expect(screen.getByText(/✓ set/i)).toBeInTheDocument();
  });

  test('"✓ Set" indicator does not render when api_key_set is false', () => {
    setup();
    expect(screen.queryByText(/✓ set/i)).not.toBeInTheDocument();
  });

  test('Save button is disabled initially (no changes)', () => {
    setup();
    const cpSection = coverProvidersSection();
    const saveButtons = within(cpSection).getAllByRole('button', { name: /save changes/i });
    // Both provider cards render Save buttons, both should be disabled initially
    for (const btn of saveButtons) {
      expect((btn as HTMLButtonElement).disabled).toBe(true);
    }
  });

  test('Save button enables after typing in API key field', async () => {
    setup();
    const theAudioDbCard = providerCard('TheAudioDB');
    const keyInput = within(theAudioDbCard).getByLabelText(/api key/i);
    await fireEvent.input(keyInput, { target: { value: 'myapikey' } });
    // The TheAudioDB save button should now be enabled
    const theAudioDbSave = within(theAudioDbCard).getByRole('button', { name: /save changes/i });
    expect(theAudioDbSave).toBeDefined();
    expect((theAudioDbSave as HTMLButtonElement).disabled).toBe(false);
  });

  test('clicking Save calls updateCoverProvider with api_key patch body', async () => {
    setup();
    (updateCoverProvider as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
      ...providerTheAudioDB,
      version_bumped: false
    });
    const theAudioDbCard = providerCard('TheAudioDB');
    const keyInput = within(theAudioDbCard).getByLabelText(/api key/i);
    await fireEvent.input(keyInput, { target: { value: 'newkey123' } });
    const btn = within(theAudioDbCard).getByRole('button', { name: /save changes/i });
    await fireEvent.click(btn);
    expect(updateCoverProvider).toHaveBeenCalledWith(
      'theaudiodb',
      expect.objectContaining({ api_key: 'newkey123' })
    );
  });

  test('when version_bumped: true, invalidateQueries is called with qk.coverage()', async () => {
    setup();
    (updateCoverProvider as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
      ...providerMbcaa,
      enabled: false,
      version_bumped: true
    });
    // Toggle enabled on MusicBrainz CAA card
    const mbcaaCard = screen.getByText('MusicBrainz CAA').closest('article')!;
    const enabledCheckbox = mbcaaCard.querySelector('input[type="checkbox"]') as HTMLInputElement;
    await fireEvent.click(enabledCheckbox);
    const saveButtons = screen.getAllByRole('button', { name: /save changes/i });
    const mbcaaSave = saveButtons.find((b) => b.closest('article')?.contains(enabledCheckbox));
    await fireEvent.click(mbcaaSave!);
    await waitFor(() => {
      expect(invalidateQueries).toHaveBeenCalledWith(
        expect.objectContaining({ queryKey: ['coverage'] })
      );
    });
  });

  test('when version_bumped: false, invalidateQueries is NOT called with qk.coverage()', async () => {
    setup();
    (updateCoverProvider as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
      ...providerMbcaa,
      enabled: false,
      version_bumped: false
    });
    const mbcaaCard = screen.getByText('MusicBrainz CAA').closest('article')!;
    const enabledCheckbox = mbcaaCard.querySelector('input[type="checkbox"]') as HTMLInputElement;
    await fireEvent.click(enabledCheckbox);
    const saveButtons = screen.getAllByRole('button', { name: /save changes/i });
    const mbcaaSave = saveButtons.find((b) => b.closest('article')?.contains(enabledCheckbox));
    await fireEvent.click(mbcaaSave!);
    await waitFor(() => {
      expect(invalidateQueries).toHaveBeenCalledWith(
        expect.objectContaining({ queryKey: ['coverProviders'] })
      );
    });
    // coverage invalidation should NOT have been called
    const coverageCalls = invalidateQueries.mock.calls.filter(
      (call: any[]) => JSON.stringify(call[0]?.queryKey) === JSON.stringify(['coverage'])
    );
    expect(coverageCalls.length).toBe(0);
  });

  test('Test connection button calls testCoverProvider and shows OK result', async () => {
    setup();
    (testCoverProvider as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
      ok: true,
      duration_ms: 120
    });
    const mbcaaCard = providerCard('MusicBrainz CAA');
    await fireEvent.click(within(mbcaaCard).getByRole('button', { name: /test connection/i }));
    await waitFor(() =>
      expect(screen.getByText(/ok.*120ms/i)).toBeInTheDocument()
    );
  });

  test('Test connection button shows failure message on error response', async () => {
    setup();
    (testCoverProvider as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
      ok: false,
      error: 'mbcaa_unreachable'
    });
    const mbcaaCard = providerCard('MusicBrainz CAA');
    await fireEvent.click(within(mbcaaCard).getByRole('button', { name: /test connection/i }));
    await waitFor(() =>
      expect(screen.getByText(/mbcaa_unreachable/i)).toBeInTheDocument()
    );
  });

  test('TheAudioDB help text renders with link', () => {
    setup();
    expect(screen.getByText(/public test key works out of the box/i)).toBeInTheDocument();
    expect(screen.getByRole('link', { name: /theaudiodb\.com\/api_apply\.php/i })).toBeInTheDocument();
  });

  test('shows loading state when cover providers query is pending', () => {
    setup({ coverProviders: null });
    expect(screen.getByText(/loading…/i)).toBeInTheDocument();
  });
});

function smtpSection(): HTMLElement {
  return screen.getByRole('heading', { name: /email \(smtp\)/i, level: 3 }).closest('section') as HTMLElement;
}

describe('/admin/integrations — SMTP card', () => {
  test('renders "Email (SMTP)" heading', () => {
    setup();
    expect(screen.getByRole('heading', { name: /email \(smtp\)/i, level: 3 })).toBeInTheDocument();
  });

  test('Save calls updateSMTPConfig with form values; empty password preserves stored value', async () => {
    setup({ smtp: smtpConfigured });
    (updateSMTPConfig as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
    // password input starts empty — should send '' which tells backend to preserve
    const section = smtpSection();
    await fireEvent.click(within(section).getByRole('button', { name: /^save$/i }));
    await waitFor(() =>
      expect(updateSMTPConfig).toHaveBeenCalledWith(
        expect.objectContaining({ password: '' })
      )
    );
  });

  test('Save with a typed password sends the typed value', async () => {
    setup({ smtp: smtpConfigured });
    (updateSMTPConfig as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
    const section = smtpSection();
    const pwInput = within(section).getByLabelText(/password/i);
    await fireEvent.input(pwInput, { target: { value: 'newsecret' } });
    await fireEvent.click(within(section).getByRole('button', { name: /^save$/i }));
    await waitFor(() =>
      expect(updateSMTPConfig).toHaveBeenCalledWith(
        expect.objectContaining({ password: 'newsecret' })
      )
    );
  });

  test('Test button calls testSMTPConfig', async () => {
    setup();
    (testSMTPConfig as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
    const section = smtpSection();
    await fireEvent.click(within(section).getByRole('button', { name: /send test email/i }));
    await waitFor(() => expect(testSMTPConfig).toHaveBeenCalled());
  });

  test('no_email_on_file error from testSMTPConfig surfaces actionable toast', async () => {
    setup();
    (testSMTPConfig as ReturnType<typeof vi.fn>).mockRejectedValueOnce({ code: 'no_email_on_file' });
    const section = smtpSection();
    await fireEvent.click(within(section).getByRole('button', { name: /send test email/i }));
    await waitFor(() =>
      expect(screen.getByTestId('toast').textContent).toMatch(/set your email in \/settings/i)
    );
  });

  test('password input placeholder shows hint when existing password is masked', async () => {
    setup({ smtp: smtpConfigured });
    // smtpConfigured has password: '***' — placeholder should show "Leave blank to keep current"
    const section = smtpSection();
    await waitFor(() =>
      expect(within(section).getByPlaceholderText(/leave blank to keep current/i)).toBeInTheDocument()
    );
  });
});

describe('Lidarr first-time setup (Test-then-Save)', () => {
  // Fresh-install variant of setup() — the three list queries return
  // empty arrays so we can verify the dropdowns get populated from the
  // test response, not from the (empty) cached query data.
  function setupFresh() {
    (createLidarrConfigQuery as ReturnType<typeof vi.fn>).mockReturnValue(
      mockQuery({ data: cfgUnset })
    );
    (createQualityProfilesQuery as ReturnType<typeof vi.fn>).mockReturnValue(mockQuery({ data: [] }));
    (createMetadataProfilesQuery as ReturnType<typeof vi.fn>).mockReturnValue(mockQuery({ data: [] }));
    (createRootFoldersQuery as ReturnType<typeof vi.fn>).mockReturnValue(mockQuery({ data: [] }));
    (createCoverProvidersQuery as ReturnType<typeof vi.fn>).mockReturnValue(
      mockQuery({ data: defaultCoverProviders })
    );
    (createSMTPConfigQuery as ReturnType<typeof vi.fn>).mockReturnValue(
      mockQuery({ data: smtpUnset })
    );
    return render(IntegrationsPage);
  }

  test('Save calls test first, fills defaults from response, then put-config', async () => {
    setupFresh();
    (testLidarrConnection as ReturnType<typeof vi.fn>).mockResolvedValueOnce(testResponseOk);
    (putLidarrConfig as ReturnType<typeof vi.fn>).mockResolvedValueOnce(cfgConnected);

    const section = lidarrSection();
    await fireEvent.input(within(section).getByLabelText(/Base URL/i), {
      target: { value: 'http://lidarr.local' }
    });
    await fireEvent.input(within(section).getByLabelText(/API key/i), {
      target: { value: 'secret-key' }
    });
    await fireEvent.click(within(section).getByRole('button', { name: /Save changes/i }));

    await waitFor(() => {
      expect(testLidarrConnection).toHaveBeenCalledWith({
        base_url: 'http://lidarr.local',
        api_key: 'secret-key'
      });
    });
    await waitFor(() => {
      expect(putLidarrConfig).toHaveBeenCalledWith({
        enabled: true,
        base_url: 'http://lidarr.local',
        api_key: 'secret-key',
        default_quality_profile_id: 1,
        default_metadata_profile_id: 1,
        default_root_folder_path: '/music'
      });
    });
  });

  test('Save aborts when test fails and surfaces the error', async () => {
    setupFresh();
    (testLidarrConnection as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
      ok: false,
      error: 'lidarr_unreachable'
    });

    const section = lidarrSection();
    await fireEvent.input(within(section).getByLabelText(/Base URL/i), {
      target: { value: 'http://wrong.local' }
    });
    await fireEvent.input(within(section).getByLabelText(/API key/i), {
      target: { value: 'k' }
    });
    await fireEvent.click(within(section).getByRole('button', { name: /Save changes/i }));

    await waitFor(() => {
      expect(testLidarrConnection).toHaveBeenCalled();
    });
    expect(putLidarrConfig).not.toHaveBeenCalled();
    await waitFor(() => {
      expect(within(lidarrSection()).getByText(/save failed — lidarr_unreachable/i)).toBeInTheDocument();
    });
  });

  test('Save preserves operator-picked defaults over auto-defaults', async () => {
    setupFresh();
    (testLidarrConnection as ReturnType<typeof vi.fn>).mockResolvedValue(testResponseOk);
    (putLidarrConfig as ReturnType<typeof vi.fn>).mockResolvedValueOnce(cfgConnected);

    const section = lidarrSection();
    await fireEvent.input(within(section).getByLabelText(/Base URL/i), {
      target: { value: 'http://lidarr.local' }
    });
    await fireEvent.input(within(section).getByLabelText(/API key/i), {
      target: { value: 'secret-key' }
    });
    // Click Test first to populate the dropdowns, then change the
    // quality profile selection before clicking Save.
    await fireEvent.click(within(section).getByRole('button', { name: /Test connection/i }));
    await waitFor(() => {
      expect(testLidarrConnection).toHaveBeenCalledTimes(1);
    });
    const qualitySelect = within(section).getByLabelText(/Default quality profile/i);
    await fireEvent.change(qualitySelect, { target: { value: '2' } });

    await fireEvent.click(within(section).getByRole('button', { name: /Save changes/i }));

    await waitFor(() => {
      expect(putLidarrConfig).toHaveBeenCalledWith(
        expect.objectContaining({
          default_quality_profile_id: 2,
          default_metadata_profile_id: 1,
          default_root_folder_path: '/music'
        })
      );
    });
  });
});

describe('/admin/integrations tag sources', () => {
  test('renders tag enrichment providers', () => {
    setup();
    const section = tagProvidersSection();
    expect(within(section).getByRole('heading', { name: 'MusicBrainz', level: 4 })).toBeInTheDocument();
    expect(within(section).getByRole('heading', { name: 'Last.fm', level: 4 })).toBeInTheDocument();
    expect(within(section).getAllByText('track tags').length).toBeGreaterThanOrEqual(1);
  });

  test('enabling Last.fm and typing a key calls updateTagProvider', async () => {
    setup();
    (updateTagProvider as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
      ...providerLastfm,
      enabled: true,
      api_key_set: true,
      version_bumped: true
    });
    const card = tagProviderCard('Last.fm');
    const enabledCheckbox = card.querySelector('input[type="checkbox"]') as HTMLInputElement;
    await fireEvent.click(enabledCheckbox);
    const keyInput = within(card).getByLabelText(/api key/i);
    await fireEvent.input(keyInput, { target: { value: 'lfmkey' } });
    await fireEvent.click(within(card).getByRole('button', { name: /save changes/i }));

    expect(updateTagProvider).toHaveBeenCalledWith(
      'lastfm',
      expect.objectContaining({ enabled: true, api_key: 'lfmkey' })
    );
    await waitFor(() =>
      expect(invalidateQueries).toHaveBeenCalledWith(
        expect.objectContaining({ queryKey: ['tagProviders'] })
      )
    );
  });

  test('Test connection on a tag provider calls testTagProvider and shows OK', async () => {
    setup();
    (testTagProvider as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ ok: true, duration_ms: 42 });
    const card = tagProviderCard('MusicBrainz');
    await fireEvent.click(within(card).getByRole('button', { name: /test connection/i }));
    await waitFor(() => expect(testTagProvider).toHaveBeenCalledWith('musicbrainz'));
    await waitFor(() => expect(within(card).getByText(/ok.*42ms/i)).toBeInTheDocument());
  });
});
