/* @vitest-environment jsdom */

import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import type { ComponentProps } from 'react';
import { afterEach, expect, test, vi } from 'vitest';

import type { CreativeAudioAsset } from './creativeAudioFileAsset';
import { CreativeAudioInputPanel } from './CreativeAudioInputPanel';

type TestAudioAsset = CreativeAudioAsset;

const originalMediaRecorder = globalThis.MediaRecorder;
const originalMediaDevices = navigator.mediaDevices;

afterEach(() => {
  globalThis.MediaRecorder = originalMediaRecorder;
  Object.defineProperty(navigator, 'mediaDevices', {
    configurable: true,
    value: originalMediaDevices,
  });
  vi.restoreAllMocks();
});

function buildAsset(overrides: Partial<TestAudioAsset> = {}): TestAudioAsset {
  return {
    assetId: 'asset-test',
    audioSrc: 'blob:audio-preview',
    audioObjectKey: '',
    assetObjectId: '',
    source: 'uploaded',
    prompt: 'hit.wav',
    durationMs: 800,
    ...overrides,
  };
}

function renderPanel(
  overrides: Partial<
    ComponentProps<typeof CreativeAudioInputPanel<TestAudioAsset>>
  > = {},
) {
  const onAssetChange = vi.fn();
  const onError = vi.fn();
  const readFileAsAsset = vi.fn(
    async (file: File, source: 'uploaded' | 'recorded') =>
      buildAsset({
        audioSrc: `blob:${source}`,
        source,
        prompt: file.name,
      }),
  );

  const rendered = render(
    <CreativeAudioInputPanel<TestAudioAsset>
      title="敲击音效"
      defaultLabel="默认木鱼音"
      asset={null}
      buildRecordedFileName={() => 'recorded-hit.webm'}
      onAssetChange={onAssetChange}
      onError={onError}
      readFileAsAsset={readFileAsAsset}
      {...overrides}
    />,
  );

  return { ...rendered, onAssetChange, onError, readFileAsAsset };
}

function getUploadInput() {
  const input = screen
    .getByText('上传')
    .closest('label')
    ?.querySelector('input[type="file"]') as HTMLInputElement | null;
  expect(input).not.toBeNull();
  return input!;
}

test('音频面板按需显示最长限制标签', () => {
  renderPanel({ limitLabel: '最长 1 秒' });

  const limitBadge = screen.getByText('最长 1 秒');

  expect(limitBadge.className).toContain('rounded-full');
  expect(limitBadge.className).toContain(
    'border-[var(--platform-subpanel-border)]',
  );
  expect(limitBadge.className).toContain('bg-[var(--platform-subpanel-fill)]');
  expect(limitBadge.className).toContain('text-[var(--platform-text-soft)]');
  expect(limitBadge.className).toContain('px-2');
  expect(limitBadge.className).toContain('py-1');
});

test('音频面板未传限制标签时不渲染限制提示', () => {
  renderPanel();

  expect(screen.queryByText('最长 1 秒')).toBeNull();
});

test('音频面板无资产时显示默认音效文案', () => {
  renderPanel();

  expect(screen.getByText('默认木鱼音')).toBeTruthy();
});

test('音频面板有预览地址时渲染 audio 控件', () => {
  const { container } = render(
    <CreativeAudioInputPanel<TestAudioAsset>
      title="敲击音效"
      defaultLabel="默认木鱼音"
      asset={buildAsset({ audioSrc: 'blob:preview' })}
      buildRecordedFileName={() => 'recorded-hit.webm'}
      onAssetChange={() => {}}
      onError={() => {}}
    />,
  );

  expect(container.querySelector('audio')?.getAttribute('src')).toBe(
    'blob:preview',
  );
});

test('音频面板有资产但无预览地址时显示已选择状态', () => {
  renderPanel({ asset: buildAsset({ audioSrc: '' }) });

  expect(screen.getByText('音效已选择')).toBeTruthy();
});

test('点击重置清空当前音频资产', () => {
  const onAssetChange = vi.fn();
  renderPanel({
    asset: buildAsset(),
    onAssetChange,
  });

  fireEvent.click(screen.getByRole('button', { name: '重置' }));

  expect(onAssetChange).toHaveBeenCalledWith(null);
});

test('取消上传选择时不读取音频', () => {
  const { readFileAsAsset, onAssetChange } = renderPanel();

  fireEvent.change(getUploadInput(), { target: { files: [] } });

  expect(readFileAsAsset).not.toHaveBeenCalled();
  expect(onAssetChange).not.toHaveBeenCalled();
});

test('上传音频成功后清空错误并写入资产', async () => {
  const audioFile = new File(['audio'], 'hit.webm', { type: 'audio/webm' });
  const { readFileAsAsset, onAssetChange, onError } = renderPanel();

  fireEvent.change(getUploadInput(), { target: { files: [audioFile] } });

  await waitFor(() =>
    expect(readFileAsAsset).toHaveBeenCalledWith(audioFile, 'uploaded'),
  );
  await waitFor(() => expect(onAssetChange).toHaveBeenCalledTimes(1));
  expect(onError).toHaveBeenCalledWith(null);
});

test('上传音频失败时提示错误且不写入资产', async () => {
  const readFileAsAsset = vi.fn(async () => {
    throw new Error('音频最长 1 秒。');
  });
  const onAssetChange = vi.fn();
  const onError = vi.fn();
  renderPanel({ readFileAsAsset, onAssetChange, onError });

  fireEvent.change(getUploadInput(), {
    target: {
      files: [new File(['audio'], 'hit.webm', { type: 'audio/webm' })],
    },
  });

  await waitFor(() => expect(onError).toHaveBeenCalledWith('音频最长 1 秒。'));
  expect(onAssetChange).not.toHaveBeenCalled();
});

test('浏览器不支持录音时提示错误', async () => {
  Object.defineProperty(navigator, 'mediaDevices', {
    configurable: true,
    value: undefined,
  });
  globalThis.MediaRecorder = undefined as unknown as typeof MediaRecorder;
  const { onError } = renderPanel();

  fireEvent.click(screen.getByRole('button', { name: '录音' }));

  await waitFor(() =>
    expect(onError).toHaveBeenCalledWith('当前浏览器不支持录音。'),
  );
});

test('录音启动失败时透传启动错误', async () => {
  Object.defineProperty(navigator, 'mediaDevices', {
    configurable: true,
    value: {
      getUserMedia: vi.fn(async () => {
        throw new Error('麦克风拒绝授权。');
      }),
    },
  });
  globalThis.MediaRecorder = class {
    start = vi.fn();
    stop = vi.fn();
  } as unknown as typeof MediaRecorder;
  const { onError } = renderPanel();

  fireEvent.click(screen.getByRole('button', { name: '录音' }));

  await waitFor(() => expect(onError).toHaveBeenCalledWith('麦克风拒绝授权。'));
});

test('录音停止后按 recorded 来源读取音频', async () => {
  const stopTrack = vi.fn();
  const recorderInstances: Array<{
    ondataavailable: ((event: BlobEvent) => void) | null;
    onstop: (() => void) | null;
  }> = [];

  Object.defineProperty(navigator, 'mediaDevices', {
    configurable: true,
    value: {
      getUserMedia: vi.fn(async () => ({
        getTracks: () => [{ stop: stopTrack }],
      })),
    },
  });
  globalThis.MediaRecorder = class {
    mimeType = 'audio/webm';
    ondataavailable: ((event: BlobEvent) => void) | null = null;
    onstop: (() => void) | null = null;

    constructor() {
      recorderInstances.push(this);
    }

    start = vi.fn();

    stop = vi.fn(() => {
      this.ondataavailable?.({
        data: new Blob(['recorded-audio'], { type: 'audio/webm' }),
      } as BlobEvent);
      this.onstop?.();
    });
  } as unknown as typeof MediaRecorder;

  const { readFileAsAsset, onAssetChange } = renderPanel();

  fireEvent.click(screen.getByRole('button', { name: '录音' }));
  await waitFor(() =>
    expect(screen.getByRole('button', { name: '停止' })).toBeTruthy(),
  );
  fireEvent.click(screen.getByRole('button', { name: '停止' }));

  await waitFor(() => expect(readFileAsAsset).toHaveBeenCalledTimes(1));
  const [recordedFile, source] = readFileAsAsset.mock.calls[0]!;
  expect(recordedFile).toBeInstanceOf(File);
  expect((recordedFile as File).name).toBe('recorded-hit.webm');
  expect(source).toBe('recorded');
  expect(stopTrack).toHaveBeenCalledTimes(1);
  await waitFor(() => expect(onAssetChange).toHaveBeenCalledTimes(1));
  expect(recorderInstances).toHaveLength(1);
});

test('录音保存失败时提示错误', async () => {
  Object.defineProperty(navigator, 'mediaDevices', {
    configurable: true,
    value: {
      getUserMedia: vi.fn(async () => ({
        getTracks: () => [],
      })),
    },
  });
  globalThis.MediaRecorder = class {
    mimeType = 'audio/webm';
    ondataavailable: ((event: BlobEvent) => void) | null = null;
    onstop: (() => void) | null = null;
    start = vi.fn();
    stop = vi.fn(() => this.onstop?.());
  } as unknown as typeof MediaRecorder;
  const readFileAsAsset = vi.fn(async () => {
    throw new Error('音频声音过小，请重新录制或上传。');
  });
  const onError = vi.fn();
  renderPanel({ readFileAsAsset, onError });

  fireEvent.click(screen.getByRole('button', { name: '录音' }));
  await waitFor(() =>
    expect(screen.getByRole('button', { name: '停止' })).toBeTruthy(),
  );
  fireEvent.click(screen.getByRole('button', { name: '停止' }));

  await waitFor(() =>
    expect(onError).toHaveBeenCalledWith('音频声音过小，请重新录制或上传。'),
  );
});

test('禁用状态不启动录音也不允许上传', () => {
  const getUserMedia = vi.fn();
  Object.defineProperty(navigator, 'mediaDevices', {
    configurable: true,
    value: { getUserMedia },
  });
  const { container } = renderPanel({ disabled: true });

  fireEvent.click(screen.getByRole('button', { name: '录音' }));

  expect(getUserMedia).not.toHaveBeenCalled();
  const input = container.querySelector('input[type="file"]');
  expect(input).not.toBeNull();
  expect((input as HTMLInputElement).disabled).toBe(true);
});
