import { beforeEach, describe, expect, it, vi } from 'vitest';
import { subscribeUnauthorized } from './auth';
import {
  downloadTroubleshootingArchive,
  downloadTroubleshootingMediaSample,
  getTroubleshootingInfo,
  getTroubleshootingPlaybackStatus,
  getTroubleshootingStreamSelectors,
  getTroubleshootingSubtitles,
  startTroubleshootingPlayback,
  validateSequentialSchedule,
  type StartTroubleshootingPlaybackBody
} from './troubleshoot';

function headerValue(init: RequestInit | undefined, name: string): string | undefined {
  const headers = init?.headers as Record<string, string> | undefined;
  if (!headers) {
    return undefined;
  }
  const match = Object.keys(headers).find((key) => key.toLowerCase() === name.toLowerCase());
  return match ? headers[match] : undefined;
}

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

const sampleInfo = {
  generalJson: '{"Version":"1.2.3"}',
  nvidiaCapabilities: 'nvidia output',
  qsvCapabilities: null,
  vaapiCapabilities: null,
  videoToolboxCapabilities: null
};

describe('getTroubleshootingInfo', () => {
  beforeEach(() => {
    window.localStorage.clear();
    vi.restoreAllMocks();
  });

  it('GETs /api/v1/troubleshoot/info and returns the info payload', async () => {
    const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(
      new Response(JSON.stringify(sampleInfo), {
        headers: { 'Content-Type': 'application/json' },
        status: 200
      })
    );

    await expect(getTroubleshootingInfo()).resolves.toMatchObject({ nvidiaCapabilities: 'nvidia output' });

    const [url, init] = fetchSpy.mock.calls[0];
    expect(url).toBe('/api/v1/troubleshoot/info');
    expect((init?.method ?? 'GET').toUpperCase()).toBe('GET');
  });

  it('rejects with the ApiError status on failure', async () => {
    vi.spyOn(window, 'fetch').mockResolvedValue(
      new Response(JSON.stringify({ status: 500, title: 'Server Error' }), {
        headers: { 'Content-Type': 'application/json' },
        status: 500
      })
    );

    await expect(getTroubleshootingInfo()).rejects.toMatchObject({ status: 500 });
  });
});

describe('validateSequentialSchedule', () => {
  const sampleResult = { isValid: true, messages: [], json: '{}' };

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

  it('POSTs /api/v1/troubleshoot/validate-schedule with the yaml and isImport body', async () => {
    const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(
      new Response(JSON.stringify(sampleResult), {
        headers: { 'Content-Type': 'application/json' },
        status: 200
      })
    );

    await expect(validateSequentialSchedule('content: []', true)).resolves.toMatchObject({ isValid: true });

    const [url, init] = fetchSpy.mock.calls[0];
    expect(url).toBe('/api/v1/troubleshoot/validate-schedule');
    expect((init?.method ?? 'GET').toUpperCase()).toBe('POST');
    expect(JSON.parse(String(init?.body))).toEqual({ yaml: 'content: []', isImport: true });
  });

  it('rejects with the ApiError status on failure', async () => {
    vi.spyOn(window, 'fetch').mockResolvedValue(
      new Response(JSON.stringify({ status: 400, title: 'Validation failed' }), {
        headers: { 'Content-Type': 'application/json' },
        status: 400
      })
    );

    await expect(validateSequentialSchedule('', false)).rejects.toMatchObject({ status: 400 });
  });
});

describe('troubleshooting playback wrappers', () => {
  beforeEach(() => {
    window.localStorage.clear();
    vi.restoreAllMocks();
  });

  it('GETs the stream selectors list', async () => {
    const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse(['a', 'b']));

    await expect(getTroubleshootingStreamSelectors()).resolves.toEqual(['a', 'b']);
    expect(fetchSpy.mock.calls[0][0]).toBe('/api/v1/troubleshoot/playback/stream-selectors');
  });

  it('GETs the subtitle list for a media item id', async () => {
    const fetchSpy = vi
      .spyOn(window, 'fetch')
      .mockResolvedValue(jsonResponse([{ id: 3, language: 'eng', title: 'English', codec: 'subrip' }]));

    await expect(getTroubleshootingSubtitles(42)).resolves.toMatchObject([{ id: 3 }]);
    expect(fetchSpy.mock.calls[0][0]).toBe('/api/v1/troubleshoot/playback/subtitles/42');
  });

  it('GETs the playback status', async () => {
    const fetchSpy = vi
      .spyOn(window, 'fetch')
      .mockResolvedValue(jsonResponse({ state: 'completed', exitCode: 0, speed: 1.2, logs: 'ok' }));

    await expect(getTroubleshootingPlaybackStatus()).resolves.toMatchObject({ state: 'completed', speed: 1.2 });
    expect(fetchSpy.mock.calls[0][0]).toBe('/api/v1/troubleshoot/playback/status');
  });
});

describe('startTroubleshootingPlayback', () => {
  beforeEach(() => {
    vi.restoreAllMocks();
  });

  it('POSTs /api/v1/troubleshoot/playback/start with the mapped body and a CSRF header, returning the url', async () => {
    const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(
      jsonResponse({ url: '/iptv/session/.troubleshooting/live.m3u8' })
    );

    // Arrays are carried through, and the mutually-exclusive pair is preserved as the caller mapped it
    // (streamSelector set → subtitleId null).
    const body: StartTroubleshootingPlaybackBody = {
      mediaItem: 5,
      channel: 0,
      ffmpegProfile: 2,
      streamingMode: 'HttpLiveStreamingSegmenter',
      watermark: [10, 11],
      graphicsElement: [20],
      streamSelector: 'selector-a',
      subtitleId: null,
      seekSeconds: 1200,
      start: null
    };

    await expect(startTroubleshootingPlayback(body)).resolves.toEqual({
      url: '/iptv/session/.troubleshooting/live.m3u8'
    });

    const [url, init] = fetchSpy.mock.calls[0];
    expect(url).toBe('/api/v1/troubleshoot/playback/start');
    expect((init?.method ?? 'GET').toUpperCase()).toBe('POST');
    expect(headerValue(init, 'X-Csrf')).toBe('1');
    expect(JSON.parse(String(init?.body))).toEqual(body);
  });

  it('rejects with the ApiError status (409) so the screen can surface "another session running"', async () => {
    vi.spyOn(window, 'fetch').mockResolvedValue(
      new Response(JSON.stringify({ status: 409, title: 'Conflict', detail: 'Another session is running.' }), {
        headers: { 'Content-Type': 'application/problem+json' },
        status: 409
      })
    );

    await expect(
      startTroubleshootingPlayback({
        mediaItem: 5,
        channel: 0,
        ffmpegProfile: 1,
        streamingMode: 'HttpLiveStreamingSegmenter',
        watermark: [],
        graphicsElement: [],
        streamSelector: null,
        subtitleId: null,
        seekSeconds: 0,
        start: null
      })
    ).rejects.toMatchObject({ status: 409, detail: 'Another session is running.' });
  });
});

describe('troubleshooting download helpers', () => {
  beforeEach(() => {
    vi.restoreAllMocks();
  });

  // Engine-neutral fake: a real `new Response(new Blob(...))` fails in CI (undici) because a
  // jsdom Blob has no `.stream()` for undici's Response body to consume ("object.stream is not a
  // function"). Passing settings through a plain object with an async `blob()` avoids any
  // Blob→Response interop crossing; `URL.createObjectURL` is stubbed so the blob body is inert.
  function zipResponse(filename: string): Response {
    return {
      ok: true,
      status: 200,
      headers: new Headers({
        'Content-Type': 'application/zip',
        'Content-Disposition': `attachment; filename="${filename}"`
      }),
      blob: async () => new Blob(['zip-bytes'])
    } as unknown as Response;
  }

  function stubBrowserDownload() {
    const createObjectURL = vi.fn(() => 'blob:mock');
    const revokeObjectURL = vi.fn();
    // jsdom does not implement these on URL; install spies so the anchor-click download path runs.
    (URL as unknown as { createObjectURL: unknown }).createObjectURL = createObjectURL;
    (URL as unknown as { revokeObjectURL: unknown }).revokeObjectURL = revokeObjectURL;
    const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
    return { createObjectURL, revokeObjectURL, clickSpy };
  }

  it('POSTs (not GETs) the archive with a CSRF header and triggers a download', async () => {
    const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(zipResponse('ersatztv-troubleshooting-123.zip'));
    const { createObjectURL, revokeObjectURL, clickSpy } = stubBrowserDownload();

    await downloadTroubleshootingArchive();

    const [url, init] = fetchSpy.mock.calls[0];
    expect(url).toBe('/api/v1/troubleshoot/playback/archive');
    expect((init?.method ?? 'GET').toUpperCase()).toBe('POST');
    expect(headerValue(init, 'X-Csrf')).toBe('1');
    expect(createObjectURL).toHaveBeenCalledTimes(1);
    expect(clickSpy).toHaveBeenCalledTimes(1);
    expect(revokeObjectURL).toHaveBeenCalledTimes(1);
  });

  it('POSTs the media sample for a given id with a CSRF header', async () => {
    const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(zipResponse('ersatztv-media-sample-123.zip'));
    stubBrowserDownload();

    await downloadTroubleshootingMediaSample(42);

    const [url, init] = fetchSpy.mock.calls[0];
    expect(url).toBe('/api/v1/troubleshoot/playback/sample/42');
    expect((init?.method ?? 'GET').toUpperCase()).toBe('POST');
    expect(headerValue(init, 'X-Csrf')).toBe('1');
  });

  it('throws an ApiError on a non-ok download response', async () => {
    vi.spyOn(window, 'fetch').mockResolvedValue(
      new Response(JSON.stringify({ status: 404, title: 'Not Found' }), {
        headers: { 'Content-Type': 'application/problem+json' },
        status: 404
      })
    );
    stubBrowserDownload();

    await expect(downloadTroubleshootingArchive()).rejects.toMatchObject({ status: 404 });
  });

  it('fires the global 401 signal on an unauthorized download response', async () => {
    // downloadViaPost raw-fetches (binary body), bypassing the central client's 401 handling, so it
    // must emit notifyUnauthorized() itself — an expired session mid-download should raise the global
    // re-login banner, not just an inline error.
    vi.spyOn(window, 'fetch').mockResolvedValue(
      new Response(JSON.stringify({ status: 401, title: 'Unauthorized' }), {
        headers: { 'Content-Type': 'application/problem+json' },
        status: 401
      })
    );
    stubBrowserDownload();
    const onUnauthorized = vi.fn();
    const unsubscribe = subscribeUnauthorized(onUnauthorized);

    await expect(downloadTroubleshootingArchive()).rejects.toMatchObject({ status: 401 });

    expect(onUnauthorized).toHaveBeenCalledTimes(1);
    unsubscribe();
  });
});
