import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { MultiCollectionsScreen } from './MultiCollectionsScreen';

function jsonResponse(body: unknown, status = 200): Response {
  return new Response(JSON.stringify(body), {
    headers: { 'Content-Type': 'application/json' },
    status
  });
}

const multiCollections = [
  {
    id: 1,
    items: [
      { collectionId: 5, name: 'Favorites', playbackOrder: 'Chronological', scheduleAsGroup: false, smartCollectionId: null },
      { collectionId: null, name: 'Action', playbackOrder: 'Chronological', scheduleAsGroup: true, smartCollectionId: 9 }
    ],
    name: 'Saturday Bundle'
  },
  { id: 2, items: [], name: 'Empty Bundle' }
];

const manualCollections = [
  { collectionType: 'Collection', id: 5, name: 'Favorites', state: 'Normal', useCustomPlaybackOrder: false }
];

const smartCollections = [{ id: 9, name: 'Action', query: 'genre:action' }];

interface MockOptions {
  multi?: unknown[];
  onRequest?: (url: string, method: string, body: unknown) => Response | null;
}

function mockApi(options: MockOptions = {}) {
  const multi = options.multi ?? multiCollections;

  return vi.spyOn(window, 'fetch').mockImplementation((input: RequestInfo | URL, init?: RequestInit) => {
    const url = input.toString();
    const method = (init?.method ?? 'GET').toUpperCase();
    const body = init?.body ? JSON.parse(String(init.body)) : undefined;

    if (options.onRequest) {
      const override = options.onRequest(url, method, body);
      if (override) {
        return Promise.resolve(override);
      }
    }

    // Single-record GET (the editor loads it on open for the #253 concurrency ETag) — must be
    // matched before the list handler, which would otherwise swallow it with the paged shape.
    const multiById = url.match(/^\/api\/v1\/multi-collections\/(\d+)$/);
    if (multiById && method === 'GET') {
      const found = (multi as Array<{ id: number }>).find((m) => String(m.id) === multiById[1]) ?? multi[0];
      return Promise.resolve(jsonResponse(found));
    }

    if (url.startsWith('/api/v1/multi-collections') && method === 'GET') {
      return Promise.resolve(jsonResponse({ page: multi, totalCount: multi.length }));
    }

    if (url === '/api/v1/collections' && method === 'GET') {
      return Promise.resolve(jsonResponse(manualCollections));
    }

    if (url === '/api/v1/smart-collections' && method === 'GET') {
      return Promise.resolve(jsonResponse(smartCollections));
    }

    return Promise.resolve(new Response(null, { status: 204 }));
  });
}

describe('MultiCollectionsScreen', () => {
  afterEach(() => {
    cleanup();
    vi.restoreAllMocks();
  });

  beforeEach(() => {
    window.localStorage.clear();
  });

  it('renders the multi-collections list with item counts', async () => {
    mockApi();
    render(<MultiCollectionsScreen />);

    expect(await screen.findByText('Saturday Bundle')).toBeInTheDocument();
    expect(screen.getByText('Empty Bundle')).toBeInTheDocument();
    expect(screen.getByText('2 items')).toBeInTheDocument();
    expect(screen.getByText('0 items')).toBeInTheDocument();
  });

  it('creates a multi-collection with an added manual collection', async () => {
    const fetchMock = mockApi({
      multi: [],
      onRequest: (url, method) =>
        url === '/api/v1/multi-collections' && method === 'POST'
          ? jsonResponse({ id: 3, items: [], name: 'New Bundle' }, 201)
          : null
    });

    render(<MultiCollectionsScreen />);

    fireEvent.click(await screen.findByRole('button', { name: 'New multi-collection' }));

    // Editor loads the collections list; select and add the manual collection.
    const collectionSelect = (await screen.findAllByRole('combobox'))[0];
    await within(collectionSelect).findByText('Favorites');
    fireEvent.change(collectionSelect, { target: { value: '5' } });
    fireEvent.click(screen.getByRole('button', { name: 'Add Collection' }));

    // The added item appears in the items list.
    expect(await screen.findByText('Favorites')).toBeInTheDocument();

    fireEvent.change(screen.getByPlaceholderText('Multi-collection name'), { target: { value: 'New Bundle' } });
    fireEvent.click(screen.getByRole('button', { name: 'Add multi-collection' }));

    await waitFor(() => {
      const postCall = fetchMock.mock.calls.find(
        ([u, init]) => u === '/api/v1/multi-collections' && (init?.method ?? '').toUpperCase() === 'POST'
      );
      expect(postCall).toBeDefined();
      expect(JSON.parse(String(postCall?.[1]?.body))).toEqual({
        items: [{ collectionId: 5, playbackOrder: 'Chronological', scheduleAsGroup: false, smartCollectionId: null, weight: 1 }],
        name: 'New Bundle'
      });
    });
  });

  it('opens an existing multi-collection and PUTs its edits', async () => {
    const fetchMock = mockApi({
      onRequest: (url, method) =>
        url === '/api/v1/multi-collections/1' && method === 'PUT'
          ? jsonResponse({ id: 1, items: [], name: 'Saturday Bundle' })
          : null
    });

    render(<MultiCollectionsScreen />);

    fireEvent.click(await screen.findByText('Saturday Bundle'));

    // Existing items are seeded from the response (manual + smart).
    expect(await screen.findByText('Favorites')).toBeInTheDocument();
    expect(screen.getByText('Action')).toBeInTheDocument();

    fireEvent.click(screen.getByRole('button', { name: 'Save multi-collection' }));

    await waitFor(() => {
      const putCall = fetchMock.mock.calls.find(
        ([u, init]) => u === '/api/v1/multi-collections/1' && (init?.method ?? '').toUpperCase() === 'PUT'
      );
      expect(putCall).toBeDefined();
      const parsed = JSON.parse(String(putCall?.[1]?.body));
      expect(parsed.name).toBe('Saturday Bundle');
      expect(parsed.items).toEqual(
        expect.arrayContaining([
          { collectionId: 5, playbackOrder: 'Chronological', scheduleAsGroup: false, smartCollectionId: null, weight: 1 },
          { collectionId: null, playbackOrder: 'Chronological', scheduleAsGroup: true, smartCollectionId: 9, weight: 1 }
        ])
      );
    });
  });

  // Weighted multi-collection fixture reused by the round-trip tests: 3:1 split (Favorites : Action).
  const weightedMulti = [
    {
      id: 1,
      items: [
        { collectionId: 5, name: 'Favorites', playbackOrder: 'Chronological', scheduleAsGroup: false, smartCollectionId: null, weight: 3 },
        { collectionId: null, name: 'Action', playbackOrder: 'Chronological', scheduleAsGroup: true, smartCollectionId: 9, weight: 1 }
      ],
      name: 'Saturday Bundle'
    }
  ];

  it('preserves weights when only an unrelated field is edited (the silent-reset trap)', async () => {
    // The canonical #404 trap: the replace-all PUT resubmits the whole item list, so editing ONLY the
    // name must still round-trip each source's weight untouched — dropping it in the draft mapping
    // would silently reset every weight to 1 on this save (docs/decisions.md 2026-07-17).
    const fetchMock = mockApi({
      multi: weightedMulti,
      onRequest: (url, method) =>
        url === '/api/v1/multi-collections/1' && method === 'PUT'
          ? jsonResponse({ id: 1, items: [], name: 'Weekend Bundle' })
          : null
    });

    render(<MultiCollectionsScreen />);
    fireEvent.click(await screen.findByText('Saturday Bundle'));
    await screen.findByLabelText('Weight for Favorites');

    // Edit ONLY the name — never touch a weight input.
    fireEvent.change(screen.getByPlaceholderText('Multi-collection name'), { target: { value: 'Weekend Bundle' } });
    fireEvent.click(screen.getByRole('button', { name: 'Save multi-collection' }));

    await waitFor(() => {
      const putCall = fetchMock.mock.calls.find(
        ([u, init]) => u === '/api/v1/multi-collections/1' && (init?.method ?? '').toUpperCase() === 'PUT'
      );
      expect(putCall).toBeDefined();
      const parsed = JSON.parse(String(putCall?.[1]?.body));
      expect(parsed.name).toBe('Weekend Bundle');
      // Both original weights survive unchanged — NOT reset to 1.
      expect(parsed.items).toEqual(
        expect.arrayContaining([
          { collectionId: 5, playbackOrder: 'Chronological', scheduleAsGroup: false, smartCollectionId: null, weight: 3 },
          { collectionId: null, playbackOrder: 'Chronological', scheduleAsGroup: true, smartCollectionId: 9, weight: 1 }
        ])
      );
    });
  });

  it('round-trips per-source weights on save (no silent reset), and edits them', async () => {
    // Weighted multi-collection: the API returns each item's weight and the update REPLACES the item
    // list, so the editor must carry weight through untouched or every source silently resets to 1
    // (docs/decisions.md 2026-07-17, #404).
    const weighted = weightedMulti;

    const fetchMock = mockApi({
      multi: weighted,
      onRequest: (url, method) =>
        url === '/api/v1/multi-collections/1' && method === 'PUT'
          ? jsonResponse({ id: 1, items: [], name: 'Saturday Bundle' })
          : null
    });

    render(<MultiCollectionsScreen />);
    fireEvent.click(await screen.findByText('Saturday Bundle'));

    // The persisted weights hydrate the inputs (75% / 25% for a 3:1 split).
    const favoritesWeight = await screen.findByLabelText('Weight for Favorites');
    expect((favoritesWeight as HTMLInputElement).value).toBe('3');
    expect(screen.getByText('75%')).toBeInTheDocument();
    expect(screen.getByText('25%')).toBeInTheDocument();

    // Edit one weight; an out-of-range value clamps to the API ceiling on blur (mirrors the validator).
    fireEvent.change(favoritesWeight, { target: { value: '5000' } });
    fireEvent.blur(favoritesWeight);
    expect((favoritesWeight as HTMLInputElement).value).toBe('1000');

    fireEvent.click(screen.getByRole('button', { name: 'Save multi-collection' }));

    await waitFor(() => {
      const putCall = fetchMock.mock.calls.find(
        ([u, init]) => u === '/api/v1/multi-collections/1' && (init?.method ?? '').toUpperCase() === 'PUT'
      );
      expect(putCall).toBeDefined();
      const parsed = JSON.parse(String(putCall?.[1]?.body));
      // Favorites clamped to 1000; Action's weight of 1 rides through unchanged (not reset).
      expect(parsed.items).toEqual(
        expect.arrayContaining([
          { collectionId: 5, playbackOrder: 'Chronological', scheduleAsGroup: false, smartCollectionId: null, weight: 1000 },
          { collectionId: null, playbackOrder: 'Chronological', scheduleAsGroup: true, smartCollectionId: 9, weight: 1 }
        ])
      );
    });
  });

  it('resets all weights to fair share', async () => {
    const weighted = [
      {
        id: 1,
        items: [
          { collectionId: 5, name: 'Favorites', playbackOrder: 'Chronological', scheduleAsGroup: false, smartCollectionId: null, weight: 3 },
          { collectionId: null, name: 'Action', playbackOrder: 'Chronological', scheduleAsGroup: true, smartCollectionId: 9, weight: 1 }
        ],
        name: 'Saturday Bundle'
      }
    ];

    mockApi({ multi: weighted });
    render(<MultiCollectionsScreen />);
    fireEvent.click(await screen.findByText('Saturday Bundle'));

    const favoritesWeight = (await screen.findByLabelText('Weight for Favorites')) as HTMLInputElement;
    expect(favoritesWeight.value).toBe('3');

    fireEvent.click(screen.getByRole('button', { name: 'Reset to fair share' }));
    expect(favoritesWeight.value).toBe('1');
    expect((screen.getByLabelText('Weight for Action') as HTMLInputElement).value).toBe('1');
  });

  it('confirms and DELETEs a multi-collection', async () => {
    const fetchMock = mockApi();

    render(<MultiCollectionsScreen />);
    const row = (await screen.findByText('Saturday Bundle')).closest('.ctv-settings-flush-row') as HTMLElement;

    fireEvent.click(within(row).getByRole('button', { name: 'Delete' }));

    const dialog = await screen.findByRole('dialog');
    fireEvent.click(within(dialog).getByRole('button', { name: 'Delete' }));

    await waitFor(() => {
      expect(
        fetchMock.mock.calls.some(
          ([u, init]) => u === '/api/v1/multi-collections/1' && (init?.method ?? '').toUpperCase() === 'DELETE'
        )
      ).toBe(true);
    });
  });
});
