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

function json(body: unknown, status = 200): Response {
  const contentType = status === 204 ? undefined : 'application/json';
  return new Response(status === 204 ? null : JSON.stringify(body), {
    headers: contentType ? { 'Content-Type': contentType } : undefined,
    status
  });
}

interface Options {
  connection: { address: string | null; hasApiKey: boolean };
  saveResponse?: Promise<Response>;
  saveStatus?: number;
  saveBody?: unknown;
}

function installFetch(family: string, options: Options) {
  const putBodies: string[] = [];
  const spy = vi.spyOn(window, 'fetch').mockImplementation((input: RequestInfo | URL, init?: RequestInit) => {
    const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
    const method = (init?.method ?? 'GET').toUpperCase();

    if (url.endsWith(`/api/v1/media-sources/${family}/connection`) && method === 'GET') {
      return Promise.resolve(json(options.connection));
    }
    if (url.endsWith(`/api/v1/media-sources/${family}/connection`) && method === 'PUT') {
      putBodies.push(String(init?.body ?? ''));
      if (options.saveResponse) {
        return options.saveResponse;
      }
      const status = options.saveStatus ?? 200;
      if (status >= 400) {
        return Promise.resolve(json({ title: 'Invalid', detail: 'Connection failed' }, status));
      }
      return Promise.resolve(json(options.saveBody ?? { address: options.connection.address, hasApiKey: true }));
    }
    return Promise.resolve(json({}, 404));
  });
  return { spy, putBodies };
}

describe('RemoteConnectionEditScreen', () => {
  beforeEach(() => {
    vi.restoreAllMocks();
    window.history.replaceState(null, '', '/app/libraries/jellyfin/connection');
  });

  afterEach(() => {
    cleanup();
  });

  it('secure key affordance: shows "leave blank to keep" and NEVER renders the stored key', async () => {
    installFetch('jellyfin', { connection: { address: 'http://jf:8096', hasApiKey: true } });

    render(<RemoteConnectionEditScreen family="jellyfin" />);

    const address = (await screen.findByPlaceholderText(/192.168/i)) as HTMLInputElement;
    expect(address.value).toBe('http://jf:8096');

    const keyField = screen.getByPlaceholderText(/leave blank to keep/i) as HTMLInputElement;
    // The stored key is never sent to the client, so the field is empty and there is no secret in the DOM.
    expect(keyField.value).toBe('');
    expect(document.body.textContent).not.toMatch(/secret|apikey-value/i);
  });

  it('blank key is retained on save (PUT body carries an empty apiKey)', async () => {
    const { putBodies } = installFetch('jellyfin', {
      connection: { address: 'http://jf:8096', hasApiKey: true },
      saveBody: { address: 'http://jf:9000', hasApiKey: true }
    });

    render(<RemoteConnectionEditScreen family="jellyfin" />);
    const address = (await screen.findByPlaceholderText(/192.168/i)) as HTMLInputElement;

    // Change only the address, leave the key blank.
    fireEvent.change(address, { target: { value: 'http://jf:9000' } });
    fireEvent.click(screen.getByRole('button', { name: 'Save connection' }));

    await vi.waitFor(() => expect(putBodies.length).toBe(1));
    const body = JSON.parse(putBodies[0]);
    expect(body.address).toBe('http://jf:9000');
    expect(body.apiKey).toBe(''); // blank = retain existing key server-side
  });

  it.each(['jellyfin', 'emby'] as const)(
    'successful %s save navigates without triggering the dirty guard',
    async (family) => {
      const editPath = `/app/libraries/${family}/connection`;
      window.history.replaceState(null, '', editPath);
      installFetch(family, {
        connection: { address: `http://${family}:8096`, hasApiKey: true },
        saveBody: { address: `http://${family}:9000`, hasApiKey: true }
      });

      const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false);
      const onPopState = () => {
        // Mirror App's dirty-guard veto: a rejected synthetic pop restores the editor path.
        if (!canLeaveCurrentScreen()) {
          window.history.pushState(null, '', editPath);
        }
      };
      window.addEventListener('popstate', onPopState);

      try {
        render(<RemoteConnectionEditScreen family={family} />);
        const address = (await screen.findByPlaceholderText(/192.168/i)) as HTMLInputElement;

        fireEvent.change(address, { target: { value: `http://${family}:9000` } });
        fireEvent.click(screen.getByRole('button', { name: 'Save connection' }));

        await vi.waitFor(() => expect(window.location.pathname).toBe(`/app/libraries/${family}`));
        expect(confirmSpy).not.toHaveBeenCalled();
      } finally {
        window.removeEventListener('popstate', onPopState);
      }
    }
  );

  it('blocks draft edits while a save is in flight so the completion cannot discard newer input', async () => {
    let resolveSave: (response: Response) => void = () => {};
    const saveResponse = new Promise<Response>((resolve) => {
      resolveSave = resolve;
    });
    const { putBodies } = installFetch('jellyfin', {
      connection: { address: 'http://jf:8096', hasApiKey: true },
      saveResponse
    });

    render(<RemoteConnectionEditScreen family="jellyfin" />);
    const address = (await screen.findByPlaceholderText(/192.168/i)) as HTMLInputElement;
    const apiKey = screen.getByPlaceholderText(/leave blank to keep/i) as HTMLInputElement;

    fireEvent.change(address, { target: { value: 'http://jf:9000' } });
    fireEvent.click(screen.getByRole('button', { name: 'Save connection' }));

    await vi.waitFor(() => expect(putBodies).toHaveLength(1));
    expect(address).toBeDisabled();
    expect(apiKey).toBeDisabled();

    // A synthetic event cannot bypass the same mutation gate used by the disabled controls.
    fireEvent.change(address, { target: { value: 'http://jf:newer' } });
    fireEvent.change(apiKey, { target: { value: 'newer-key' } });
    expect(address.value).toBe('http://jf:9000');
    expect(apiKey.value).toBe('');

    resolveSave(json({ address: 'http://jf:9000', hasApiKey: true }));
    await vi.waitFor(() => expect(window.location.pathname).toBe('/app/libraries/jellyfin'));
    expect(putBodies).toHaveLength(1);
  });

  it('first connect (no stored key) requires an api key before Save enables', async () => {
    installFetch('jellyfin', { connection: { address: '', hasApiKey: false } });

    render(<RemoteConnectionEditScreen family="jellyfin" />);
    const address = (await screen.findByPlaceholderText(/192.168/i)) as HTMLInputElement;

    fireEvent.change(address, { target: { value: 'http://jf:8096' } });
    // Address valid but no key yet: Connect stays disabled.
    const connect = screen.getByRole('button', { name: 'Connect' }) as HTMLButtonElement;
    expect(connect.disabled).toBe(true);

    fireEvent.change(screen.getByPlaceholderText(/Enter an API key/i), { target: { value: 'newkey123' } });
    expect((screen.getByRole('button', { name: 'Connect' }) as HTMLButtonElement).disabled).toBe(false);
  });

  it('retains the draft on a 422 save failure', async () => {
    const { putBodies } = installFetch('jellyfin', {
      connection: { address: 'http://jf:8096', hasApiKey: true },
      saveStatus: 422
    });

    render(<RemoteConnectionEditScreen family="jellyfin" />);
    const address = (await screen.findByPlaceholderText(/192.168/i)) as HTMLInputElement;

    fireEvent.change(address, { target: { value: 'http://jf:9999' } });
    fireEvent.click(screen.getByRole('button', { name: 'Save connection' }));

    await vi.waitFor(() => expect(putBodies.length).toBe(1));
    // Draft not reset: the edited address is still present and the error is surfaced.
    expect((screen.getByPlaceholderText(/192.168/i) as HTMLInputElement).value).toBe('http://jf:9999');
    expect(await screen.findByText(/Connection failed/i)).toBeTruthy();
  });

  it('dirty guard vetoes navigation until confirmed', async () => {
    installFetch('jellyfin', { connection: { address: 'http://jf:8096', hasApiKey: true } });

    render(<RemoteConnectionEditScreen family="jellyfin" />);
    const address = (await screen.findByPlaceholderText(/192.168/i)) as HTMLInputElement;

    // Clean: guard allows leaving.
    expect(canLeaveCurrentScreen()).toBe(true);

    fireEvent.change(address, { target: { value: 'http://jf:changed' } });
    // Let the dirty effect commit.
    await vi.waitFor(() => expect((screen.getByPlaceholderText(/192.168/i) as HTMLInputElement).value).toBe('http://jf:changed'));

    const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false);
    expect(canLeaveCurrentScreen()).toBe(false); // dirty + confirm=false → vetoed
    confirmSpy.mockReturnValue(true);
    expect(canLeaveCurrentScreen()).toBe(true); // confirm=true → allowed
    confirmSpy.mockRestore();
  });
});
