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

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

const groups = [
  { id: 2, name: 'Prime', templateCount: 1 },
  { id: 3, name: 'Overnight', templateCount: 1 }
];
const templates = [
  { id: 4, templateGroupId: 2, groupName: 'Prime', name: 'Weekdays' },
  { id: 6, templateGroupId: 2, groupName: 'Prime', name: 'Weekends' },
  { id: 9, templateGroupId: 3, groupName: 'Overnight', name: 'Late Show' }
];
const blockGroups = [{ id: 1, name: 'Morning Blocks' }];
const blocks = [{ id: 10, groupId: 1, groupName: 'Morning Blocks', name: 'Cartoons', minutes: 60, stopScheduling: 'AfterDurationEnd' }];

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

function mockApi(options: MockOptions = {}) {
  const items = options.items ?? [];

  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);
      }
    }

    if (url === '/api/v1/templates/groups' && method === 'GET') {
      return Promise.resolve(jsonResponse(groups));
    }
    if (url === '/api/v1/templates' && method === 'GET') {
      return Promise.resolve(jsonResponse(templates));
    }
    if (url === '/api/v1/templates/4' && method === 'GET') {
      return Promise.resolve(jsonResponse(templates[0]));
    }
    if (url === '/api/v1/templates/4/items' && method === 'GET') {
      return Promise.resolve(jsonResponse(items));
    }
    if (url === '/api/v1/blocks/groups' && method === 'GET') {
      return Promise.resolve(jsonResponse(blockGroups));
    }
    if (url === '/api/v1/blocks' && method === 'GET') {
      return Promise.resolve(jsonResponse(blocks));
    }
    if (url === '/api/v1/templates/4' && method === 'PUT') {
      return Promise.resolve(jsonResponse({ ...templates[0], items: [] }));
    }
    if (url === '/api/v1/templates/4/copy' && method === 'POST') {
      return Promise.resolve(jsonResponse({ id: 5, templateGroupId: 2, groupName: 'Prime', name: 'Weekdays copy' }, 201));
    }

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

describe('TemplatesScreen', () => {
  afterEach(() => {
    cleanup();
    vi.restoreAllMocks();
    window.history.pushState({}, '', '/app/templates');
  });

  beforeEach(() => {
    window.localStorage.clear();
    window.history.pushState({}, '', '/app/templates');
  });

  it('renders template groups and their templates', async () => {
    mockApi();
    render(<TemplatesScreen />);

    expect(await screen.findByText('Prime')).toBeInTheDocument();
    expect(screen.getByText('Weekdays')).toBeInTheDocument();
  });

  it('filters templates by name, case-insensitive', async () => {
    mockApi();
    render(<TemplatesScreen />);

    await screen.findByText('Weekdays');
    expect(screen.getByText('Weekends')).toBeInTheDocument();
    expect(screen.getByText('Late Show')).toBeInTheDocument();

    fireEvent.change(screen.getByPlaceholderText('Search for templates…'), { target: { value: 'weekEND' } });

    expect(screen.getByText('Weekends')).toBeInTheDocument();
    expect(screen.queryByText('Weekdays')).not.toBeInTheDocument();
    expect(screen.queryByText('Late Show')).not.toBeInTheDocument();
  });

  it('filters templates by group name and shows an empty state for no matches', async () => {
    mockApi();
    render(<TemplatesScreen />);

    await screen.findByText('Weekdays');

    fireEvent.change(screen.getByPlaceholderText('Search for templates…'), { target: { value: 'overnight' } });
    expect(screen.getByText('Late Show')).toBeInTheDocument();
    expect(screen.queryByText('Weekdays')).not.toBeInTheDocument();

    fireEvent.change(screen.getByPlaceholderText('Search for templates…'), { target: { value: 'zzz-no-match' } });
    expect(await screen.findByText('No templates match this filter.')).toBeInTheDocument();
  });

  it('opens the copy dialog and POSTs to the copy route', async () => {
    mockApi();
    render(<TemplatesScreen />);

    await screen.findByText('Weekdays');
    const row = screen.getByText('Weekdays').closest('.ctv-settings-flush-row') as HTMLElement;
    fireEvent.click(within(row).getByRole('button', { name: /Copy template/ }));

    const dialog = await screen.findByText('Copy "Weekdays"');
    expect(dialog).toBeInTheDocument();

    const fetchMock = vi.spyOn(window, 'fetch');
    fireEvent.click(screen.getByRole('button', { name: 'Copy' }));

    await waitFor(() => {
      const call = fetchMock.mock.calls.find(([u, init]) => u === '/api/v1/templates/4/copy' && init?.method === 'POST');
      expect(call).toBeTruthy();
    });
  });

  it('loads the editor at a sub-path and lists items sorted by start time', async () => {
    window.history.pushState({}, '', '/app/templates/4');
    mockApi({
      items: [
        { blockId: 10, blockName: 'Cartoons', blockMinutes: 60, startTime: '12:00:00' },
        { blockId: 10, blockName: 'Cartoons', blockMinutes: 60, startTime: '06:00:00' }
      ]
    });
    render(<TemplatesScreen />);

    await waitFor(() => expect(screen.getByDisplayValue('Weekdays')).toBeInTheDocument());

    const rows = screen.getAllByText(/^\d{2}:\d{2}-\d{2}:\d{2}$/);
    expect(rows[0]).toHaveTextContent('06:00-07:00');
    expect(rows[1]).toHaveTextContent('12:00-13:00');
  });

  it('adds an item via the block group/block/start-time pickers', async () => {
    window.history.pushState({}, '', '/app/templates/4');
    mockApi({ items: [] });
    render(<TemplatesScreen />);

    await waitFor(() => expect(screen.getByDisplayValue('Weekdays')).toBeInTheDocument());

    fireEvent.change(screen.getByLabelText('Block group'), { target: { value: '1' } });
    fireEvent.change(screen.getByLabelText('Block'), { target: { value: '10' } });
    fireEvent.click(screen.getByRole('button', { name: 'Add' }));

    const cell = await screen.findByRole('cell', { name: 'Cartoons' });
    expect(cell).toBeInTheDocument();
  });

  it('saves the template with items via PUT', async () => {
    window.history.pushState({}, '', '/app/templates/4');
    const fetchMock = mockApi({
      items: [{ blockId: 10, blockName: 'Cartoons', blockMinutes: 60, startTime: '06:00:00' }]
    });
    render(<TemplatesScreen />);

    await waitFor(() => expect(screen.getByDisplayValue('Weekdays')).toBeInTheDocument());
    fireEvent.click(screen.getByRole('button', { name: /Save template/ }));

    await waitFor(() => {
      const call = fetchMock.mock.calls.find(([u, init]) => u === '/api/v1/templates/4' && init?.method === 'PUT');
      expect(call).toBeTruthy();
    });

    const putCall = fetchMock.mock.calls.find(([u, init]) => u === '/api/v1/templates/4' && init?.method === 'PUT');
    const sent = JSON.parse(String(putCall?.[1]?.body));
    expect(sent.name).toBe('Weekdays');
    expect(sent.items).toEqual([{ blockId: 10, startTime: '06:00:00' }]);
  });

  it('excludes the synthesized negative-id "(none)" placeholder row from the block picker', async () => {
    window.history.pushState({}, '', '/app/templates/4');
    mockApi({
      items: [],
      onRequest: (url, method) => {
        if (url === '/api/v1/blocks' && method === 'GET') {
          return jsonResponse([
            ...blocks,
            { id: -1, groupId: 1, groupName: 'Morning Blocks', name: '(none)', minutes: 0, stopScheduling: 'AfterDurationEnd' }
          ]);
        }
        return null;
      }
    });
    render(<TemplatesScreen />);

    await waitFor(() => expect(screen.getByDisplayValue('Weekdays')).toBeInTheDocument());

    fireEvent.change(screen.getByLabelText('Block group'), { target: { value: '1' } });

    const blockSelect = screen.getByLabelText('Block') as HTMLSelectElement;
    const optionValues = Array.from(blockSelect.options).map((o) => o.value);
    expect(optionValues).toEqual(['', '10']);
  });

  it('shows a midnight-crossing warning and blocks save when an item crosses midnight', async () => {
    window.history.pushState({}, '', '/app/templates/4');
    mockApi({
      items: [{ blockId: 10, blockName: 'Cartoons', blockMinutes: 60, startTime: '23:30:00' }]
    });
    render(<TemplatesScreen />);

    await waitFor(() => expect(screen.getByDisplayValue('Weekdays')).toBeInTheDocument());

    expect(await screen.findByText(/crosses midnight/)).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /Save template/ })).toBeDisabled();
  });

  it('shows an overlap warning and blocks save when two items intersect', async () => {
    window.history.pushState({}, '', '/app/templates/4');
    mockApi({
      items: [
        { blockId: 10, blockName: 'Cartoons', blockMinutes: 60, startTime: '06:00:00' },
        { blockId: 10, blockName: 'Cartoons', blockMinutes: 60, startTime: '06:30:00' }
      ]
    });
    render(<TemplatesScreen />);

    await waitFor(() => expect(screen.getByDisplayValue('Weekdays')).toBeInTheDocument());

    expect(await screen.findByText(/overlaps/)).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /Save template/ })).toBeDisabled();
  });

  it('shows a conflict dialog and reloads when the template changed elsewhere (412)', async () => {
    window.history.pushState({}, '', '/app/templates/4');
    let putCount = 0;
    const fetchMock = mockApi({
      items: [{ blockId: 10, blockName: 'Cartoons', blockMinutes: 60, startTime: '06:00:00' }],
      onRequest: (url, method) => {
        if (url === '/api/v1/templates/4' && method === 'PUT') {
          putCount += 1;
          if (putCount === 1) {
            return new Response(
              JSON.stringify({ status: 412, title: 'Precondition Failed', detail: 'stale' }),
              { headers: { 'Content-Type': 'application/json' }, status: 412 }
            );
          }
        }
        return null;
      }
    });
    render(<TemplatesScreen />);

    await waitFor(() => expect(screen.getByDisplayValue('Weekdays')).toBeInTheDocument());

    const itemsGetCount = () =>
      fetchMock.mock.calls.filter(([u, init]) => u === '/api/v1/templates/4/items' && (init?.method ?? 'GET') === 'GET')
        .length;
    const before = itemsGetCount();

    fireEvent.click(screen.getByRole('button', { name: /Save template/ }));

    // A 412 opens the "changed elsewhere" dialog rather than showing a generic save error.
    expect(await screen.findByText(/Reload to get the latest version/i)).toBeInTheDocument();

    // Reloading re-fetches the template items.
    fireEvent.click(screen.getByRole('button', { name: /^Reload$/ }));
    await waitFor(() => expect(itemsGetCount()).toBeGreaterThan(before));
  });
});
