import assert from 'node:assert/strict';
import test from 'node:test';

import { CardCreationService } from './card-creation';
import { toMpvEdlValue } from './mpv-edl-test-utils';
import type { MediaInput } from '../media-generator';
import type { AnkiConnectConfig } from '../types/anki';

test('CardCreationService counts locally created sentence cards', async () => {
  const minedCards: Array<{ count: number; noteIds?: number[] }> = [];
  const service = new CardCreationService({
    getConfig: () =>
      ({
        deck: 'Mining',
        fields: {
          sentence: 'Sentence',
          audio: 'SentenceAudio',
        },
        media: {
          generateAudio: false,
          generateImage: false,
        },
        behavior: {},
        ai: false,
      }) as AnkiConnectConfig,
    getAiConfig: () => ({}),
    getTimingTracker: () => ({}) as never,
    getMpvClient: () =>
      ({
        currentVideoPath: '/video.mp4',
        currentSubText: '字幕',
        currentSubStart: 1,
        currentSubEnd: 2,
        currentTimePos: 1.5,
        currentAudioStreamIndex: 0,
      }) as never,
    client: {
      addNote: async () => 42,
      addTags: async () => undefined,
      notesInfo: async () => [],
      updateNoteFields: async () => undefined,
      storeMediaFile: async () => undefined,
      findNotes: async () => [],
      retrieveMediaFile: async () => '',
      deleteNotes: async () => undefined,
    },
    mediaGenerator: {
      generateAudio: async () => null,
      generateScreenshot: async () => null,
      generateAnimatedImage: async () => null,
    },
    showOsdNotification: () => undefined,
    showUpdateResult: () => undefined,
    showStatusNotification: () => undefined,
    showNotification: async () => undefined,
    beginUpdateProgress: () => undefined,
    endUpdateProgress: () => undefined,
    withUpdateProgress: async (_message, action) => action(),
    resolveConfiguredFieldName: () => null,
    resolveNoteFieldName: () => null,
    getAnimatedImageLeadInSeconds: async () => 0,
    extractFields: () => ({}),
    processSentence: (sentence) => sentence,
    setCardTypeFields: () => undefined,
    mergeFieldValue: (_existing, newValue) => newValue,
    formatMiscInfoPattern: () => '',
    getEffectiveSentenceCardConfig: () => ({
      model: 'Sentence',
      sentenceField: 'Sentence',
      audioField: 'SentenceAudio',
      lapisEnabled: false,
      kikuEnabled: false,
      fieldGroupingMode: 'disabled',
    }),
    getFallbackDurationSeconds: () => 10,
    appendKnownWordsFromNoteInfo: () => undefined,
    removeKnownWordNote: () => undefined,
    isUpdateInProgress: () => false,
    setUpdateInProgress: () => undefined,
    trackLastAddedNoteId: () => undefined,
    recordCardsMinedCallback: (count, noteIds) => {
      minedCards.push({ count, noteIds });
    },
  });

  const created = await service.createSentenceCard('テスト', 0, 1);

  assert.equal(created, true);
  assert.deepEqual(minedCards, [{ count: 1, noteIds: [42] }]);
});

test('CardCreationService keeps updating after trackLastAddedNoteId throws', async () => {
  const calls = {
    notesInfo: 0,
    updateNoteFields: 0,
  };
  const service = new CardCreationService({
    getConfig: () =>
      ({
        deck: 'Mining',
        fields: {
          sentence: 'Sentence',
          audio: 'SentenceAudio',
        },
        media: {
          generateAudio: false,
          generateImage: false,
        },
        behavior: {},
        ai: false,
      }) as AnkiConnectConfig,
    getAiConfig: () => ({}),
    getTimingTracker: () => ({}) as never,
    getMpvClient: () =>
      ({
        currentVideoPath: '/video.mp4',
        currentSubText: '字幕',
        currentSubStart: 1,
        currentSubEnd: 2,
        currentTimePos: 1.5,
        currentAudioStreamIndex: 0,
      }) as never,
    client: {
      addNote: async () => 42,
      addTags: async () => undefined,
      notesInfo: async () => {
        calls.notesInfo += 1;
        return [
          {
            noteId: 42,
            fields: {
              Sentence: { value: 'existing' },
            },
          },
        ];
      },
      updateNoteFields: async () => {
        calls.updateNoteFields += 1;
      },
      storeMediaFile: async () => undefined,
      findNotes: async () => [],
      retrieveMediaFile: async () => '',
      deleteNotes: async () => undefined,
    },
    mediaGenerator: {
      generateAudio: async () => null,
      generateScreenshot: async () => null,
      generateAnimatedImage: async () => null,
    },
    showOsdNotification: () => undefined,
    showUpdateResult: () => undefined,
    showStatusNotification: () => undefined,
    showNotification: async () => undefined,
    beginUpdateProgress: () => undefined,
    endUpdateProgress: () => undefined,
    withUpdateProgress: async (_message, action) => action(),
    resolveConfiguredFieldName: () => null,
    resolveNoteFieldName: () => null,
    getAnimatedImageLeadInSeconds: async () => 0,
    extractFields: () => ({}),
    processSentence: (sentence) => sentence,
    setCardTypeFields: (updatedFields) => {
      updatedFields.CardType = 'sentence';
    },
    mergeFieldValue: (_existing, newValue) => newValue,
    formatMiscInfoPattern: () => '',
    getEffectiveSentenceCardConfig: () => ({
      model: 'Sentence',
      sentenceField: 'Sentence',
      audioField: 'SentenceAudio',
      lapisEnabled: false,
      kikuEnabled: false,
      fieldGroupingMode: 'disabled',
    }),
    getFallbackDurationSeconds: () => 10,
    appendKnownWordsFromNoteInfo: () => undefined,
    removeKnownWordNote: () => undefined,
    isUpdateInProgress: () => false,
    setUpdateInProgress: () => undefined,
    trackLastAddedNoteId: () => {
      throw new Error('track failed');
    },
  });

  const created = await service.createSentenceCard('テスト', 0, 1);

  assert.equal(created, true);
  assert.equal(calls.notesInfo, 1);
  assert.equal(calls.updateNoteFields, 1);
});

test('CardCreationService keeps updating after recordCardsMinedCallback throws', async () => {
  const calls = {
    notesInfo: 0,
    updateNoteFields: 0,
  };
  const service = new CardCreationService({
    getConfig: () =>
      ({
        deck: 'Mining',
        fields: {
          sentence: 'Sentence',
          audio: 'SentenceAudio',
        },
        media: {
          generateAudio: false,
          generateImage: false,
        },
        behavior: {},
        ai: false,
      }) as AnkiConnectConfig,
    getAiConfig: () => ({}),
    getTimingTracker: () => ({}) as never,
    getMpvClient: () =>
      ({
        currentVideoPath: '/video.mp4',
        currentSubText: '字幕',
        currentSubStart: 1,
        currentSubEnd: 2,
        currentTimePos: 1.5,
        currentAudioStreamIndex: 0,
      }) as never,
    client: {
      addNote: async () => 42,
      addTags: async () => undefined,
      notesInfo: async () => {
        calls.notesInfo += 1;
        return [
          {
            noteId: 42,
            fields: {
              Sentence: { value: 'existing' },
            },
          },
        ];
      },
      updateNoteFields: async () => {
        calls.updateNoteFields += 1;
      },
      storeMediaFile: async () => undefined,
      findNotes: async () => [],
      retrieveMediaFile: async () => '',
      deleteNotes: async () => undefined,
    },
    mediaGenerator: {
      generateAudio: async () => null,
      generateScreenshot: async () => null,
      generateAnimatedImage: async () => null,
    },
    showOsdNotification: () => undefined,
    showUpdateResult: () => undefined,
    showStatusNotification: () => undefined,
    showNotification: async () => undefined,
    beginUpdateProgress: () => undefined,
    endUpdateProgress: () => undefined,
    withUpdateProgress: async (_message, action) => action(),
    resolveConfiguredFieldName: () => null,
    resolveNoteFieldName: () => null,
    getAnimatedImageLeadInSeconds: async () => 0,
    extractFields: () => ({}),
    processSentence: (sentence) => sentence,
    setCardTypeFields: (updatedFields) => {
      updatedFields.CardType = 'sentence';
    },
    mergeFieldValue: (_existing, newValue) => newValue,
    formatMiscInfoPattern: () => '',
    getEffectiveSentenceCardConfig: () => ({
      model: 'Sentence',
      sentenceField: 'Sentence',
      audioField: 'SentenceAudio',
      lapisEnabled: false,
      kikuEnabled: false,
      fieldGroupingMode: 'disabled',
    }),
    getFallbackDurationSeconds: () => 10,
    appendKnownWordsFromNoteInfo: () => undefined,
    removeKnownWordNote: () => undefined,
    isUpdateInProgress: () => false,
    setUpdateInProgress: () => undefined,
    recordCardsMinedCallback: () => {
      throw new Error('record failed');
    },
  });

  const created = await service.createSentenceCard('テスト', 0, 1);

  assert.equal(created, true);
  assert.equal(calls.notesInfo, 1);
  assert.equal(calls.updateNoteFields, 1);
});

test('CardCreationService uses stream-open-filename for remote media generation', async () => {
  const audioPaths: string[] = [];
  const imagePaths: string[] = [];
  const recordMediaPath = (mediaInput: MediaInput): string =>
    typeof mediaInput === 'string' ? mediaInput : mediaInput.path;
  const audioUrl = 'https://audio.example/videoplayback?mime=audio%2Fwebm';
  const videoUrl = 'https://video.example/videoplayback?mime=video%2Fmp4';
  const edlSource = [
    `edl://!new_stream;!no_clip;!no_chapters;${toMpvEdlValue(audioUrl)}`,
    `!new_stream;!no_clip;!no_chapters;${toMpvEdlValue(videoUrl)}`,
    '!global_tags,title=test',
  ].join(';');

  const service = new CardCreationService({
    getConfig: () =>
      ({
        deck: 'Mining',
        fields: {
          sentence: 'Sentence',
          audio: 'SentenceAudio',
          image: 'Picture',
        },
        media: {
          generateAudio: true,
          generateImage: true,
          imageFormat: 'jpg',
        },
        behavior: {},
        ai: false,
      }) as AnkiConnectConfig,
    getAiConfig: () => ({}),
    getTimingTracker: () => ({}) as never,
    getMpvClient: () =>
      ({
        currentVideoPath: 'https://www.youtube.com/watch?v=abc123',
        currentSubText: '字幕',
        currentSubStart: 1,
        currentSubEnd: 2,
        currentTimePos: 1.5,
        currentAudioStreamIndex: 0,
        requestProperty: async (name: string) => {
          assert.equal(name, 'stream-open-filename');
          return edlSource;
        },
      }) as never,
    client: {
      addNote: async () => 42,
      addTags: async () => undefined,
      notesInfo: async () => [
        {
          noteId: 42,
          fields: {
            Sentence: { value: '' },
            SentenceAudio: { value: '' },
            Picture: { value: '' },
          },
        },
      ],
      updateNoteFields: async () => undefined,
      storeMediaFile: async () => undefined,
      findNotes: async () => [],
      retrieveMediaFile: async () => '',
      deleteNotes: async () => undefined,
    },
    mediaGenerator: {
      generateAudio: async (path) => {
        audioPaths.push(recordMediaPath(path));
        return Buffer.from('audio');
      },
      generateScreenshot: async (path) => {
        imagePaths.push(recordMediaPath(path));
        return Buffer.from('image');
      },
      generateAnimatedImage: async () => null,
    },
    showOsdNotification: () => undefined,
    showUpdateResult: () => undefined,
    showStatusNotification: () => undefined,
    showNotification: async () => undefined,
    beginUpdateProgress: () => undefined,
    endUpdateProgress: () => undefined,
    withUpdateProgress: async (_message, action) => action(),
    resolveConfiguredFieldName: (noteInfo, preferredName) => {
      if (!preferredName) return null;
      return Object.keys(noteInfo.fields).find((field) => field === preferredName) ?? null;
    },
    resolveNoteFieldName: (noteInfo, preferredName) => {
      if (!preferredName) return null;
      return Object.keys(noteInfo.fields).find((field) => field === preferredName) ?? null;
    },
    getAnimatedImageLeadInSeconds: async () => 0,
    extractFields: () => ({}),
    processSentence: (sentence) => sentence,
    setCardTypeFields: () => undefined,
    mergeFieldValue: (_existing, newValue) => newValue,
    formatMiscInfoPattern: () => '',
    getEffectiveSentenceCardConfig: () => ({
      model: 'Sentence',
      sentenceField: 'Sentence',
      audioField: 'SentenceAudio',
      lapisEnabled: false,
      kikuEnabled: false,
      fieldGroupingMode: 'disabled',
    }),
    getFallbackDurationSeconds: () => 10,
    appendKnownWordsFromNoteInfo: () => undefined,
    removeKnownWordNote: () => undefined,
    isUpdateInProgress: () => false,
    setUpdateInProgress: () => undefined,
    trackLastAddedNoteId: () => undefined,
  });

  const created = await service.createSentenceCard('テスト', 0, 1);

  assert.equal(created, true);
  assert.deepEqual(audioPaths, [audioUrl]);
  assert.deepEqual(imagePaths, [videoUrl]);
});

test('CardCreationService does not use mpv stream indexes for ready cached YouTube media', async () => {
  const audioCalls: Array<{ path: string; audioStreamIndex?: number }> = [];

  const service = new CardCreationService({
    getConfig: () =>
      ({
        deck: 'Mining',
        fields: {
          sentence: 'Sentence',
          audio: 'SentenceAudio',
          image: 'Picture',
        },
        media: {
          generateAudio: true,
          generateImage: false,
          imageFormat: 'jpg',
        },
        behavior: {},
        ai: false,
      }) as AnkiConnectConfig,
    getAiConfig: () => ({}),
    getTimingTracker: () => ({}) as never,
    getMpvClient: () =>
      ({
        currentVideoPath: 'https://www.youtube.com/watch?v=abc123',
        currentSubText: '字幕',
        currentSubStart: 10,
        currentSubEnd: 12,
        currentTimePos: 11,
        currentAudioStreamIndex: 0,
      }) as never,
    getCachedMediaPath: async () => '/tmp/subminer-youtube-media-cache/media.mkv',
    shouldRequireRemoteMediaCache: () => true,
    client: {
      addNote: async () => 42,
      addTags: async () => undefined,
      notesInfo: async () => [
        {
          noteId: 42,
          fields: {
            Sentence: { value: '' },
            SentenceAudio: { value: '' },
            Picture: { value: '' },
          },
        },
      ],
      updateNoteFields: async () => undefined,
      storeMediaFile: async () => undefined,
      findNotes: async () => [],
      retrieveMediaFile: async () => '',
      deleteNotes: async () => undefined,
    },
    mediaGenerator: {
      generateAudio: async (path, _startTime, _endTime, _padding, audioStreamIndex) => {
        audioCalls.push({ path: typeof path === 'string' ? path : path.path, audioStreamIndex });
        return Buffer.from('audio');
      },
      generateScreenshot: async () => null,
      generateAnimatedImage: async () => null,
    },
    showOsdNotification: () => undefined,
    showUpdateResult: () => undefined,
    showStatusNotification: () => undefined,
    showNotification: async () => undefined,
    beginUpdateProgress: () => undefined,
    endUpdateProgress: () => undefined,
    withUpdateProgress: async (_message, action) => action(),
    resolveConfiguredFieldName: (noteInfo, preferredName) => {
      if (!preferredName) return null;
      return Object.keys(noteInfo.fields).find((field) => field === preferredName) ?? null;
    },
    resolveNoteFieldName: (noteInfo, preferredName) => {
      if (!preferredName) return null;
      return Object.keys(noteInfo.fields).find((field) => field === preferredName) ?? null;
    },
    getAnimatedImageLeadInSeconds: async () => 0,
    extractFields: () => ({}),
    processSentence: (sentence) => sentence,
    setCardTypeFields: () => undefined,
    mergeFieldValue: (_existing, newValue) => newValue,
    formatMiscInfoPattern: () => '',
    getEffectiveSentenceCardConfig: () => ({
      model: 'Sentence',
      sentenceField: 'Sentence',
      audioField: 'SentenceAudio',
      lapisEnabled: false,
      kikuEnabled: false,
      fieldGroupingMode: 'disabled',
    }),
    getFallbackDurationSeconds: () => 10,
    appendKnownWordsFromNoteInfo: () => undefined,
    removeKnownWordNote: () => undefined,
    isUpdateInProgress: () => false,
    setUpdateInProgress: () => undefined,
    trackLastAddedNoteId: () => undefined,
  });

  const created = await service.createSentenceCard('テスト', 10, 12);

  assert.equal(created, true);
  assert.deepEqual(audioCalls, [
    {
      path: '/tmp/subminer-youtube-media-cache/media.mkv',
      audioStreamIndex: undefined,
    },
  ]);
});

test('CardCreationService queues YouTube media when required cache is not ready', async () => {
  const mediaCalls: string[] = [];
  const updates: Array<{ noteId: number; fields: Record<string, string> }> = [];
  const queuedUpdates: Array<{
    sourceUrl: string;
    noteId: number;
    startTime: number;
    endTime: number;
    label: string | number;
    audioFieldName?: string;
    imageFieldName?: string;
    miscInfoFieldName?: string;
    generateAudio: boolean;
    generateImage: boolean;
    volumeScale?: number;
  }> = [];
  let streamRequests = 0;

  const service = new CardCreationService({
    getConfig: () =>
      ({
        deck: 'Mining',
        fields: {
          sentence: 'Sentence',
          audio: 'SentenceAudio',
          image: 'Picture',
          miscInfo: 'MiscInfo',
        },
        media: {
          generateAudio: true,
          generateImage: true,
          imageFormat: 'jpg',
        },
        behavior: {},
        ai: false,
      }) as AnkiConnectConfig,
    getAiConfig: () => ({}),
    getTimingTracker: () => ({}) as never,
    getMpvClient: () =>
      ({
        currentVideoPath: 'https://www.youtube.com/watch?v=abc123',
        currentSubText: '字幕',
        currentSubStart: 10,
        currentSubEnd: 12,
        currentTimePos: 11,
        currentAudioStreamIndex: 2,
        requestProperty: async (name: string) => {
          if (name === 'volume') {
            return 35;
          }
          streamRequests += 1;
          return 'https://rr1---sn.example.googlevideo.com/videoplayback?id=123';
        },
      }) as never,
    getCachedMediaPath: async () => null,
    shouldRequireRemoteMediaCache: () => true,
    queuePendingYoutubeMediaUpdate: (job) => {
      queuedUpdates.push(job);
    },
    client: {
      addNote: async () => 42,
      addTags: async () => undefined,
      notesInfo: async () => [
        {
          noteId: 42,
          fields: {
            Sentence: { value: '' },
            SentenceAudio: { value: '' },
            Picture: { value: '' },
            MiscInfo: { value: '' },
          },
        },
      ],
      updateNoteFields: async (noteId, fields) => {
        updates.push({ noteId, fields });
      },
      storeMediaFile: async () => undefined,
      findNotes: async () => [],
      retrieveMediaFile: async () => '',
      deleteNotes: async () => undefined,
    },
    mediaGenerator: {
      generateAudio: async () => {
        mediaCalls.push('audio');
        return Buffer.from('audio');
      },
      generateScreenshot: async () => {
        mediaCalls.push('image');
        return Buffer.from('image');
      },
      generateAnimatedImage: async () => null,
    },
    showOsdNotification: () => undefined,
    showUpdateResult: () => undefined,
    showStatusNotification: () => undefined,
    showNotification: async () => undefined,
    beginUpdateProgress: () => undefined,
    endUpdateProgress: () => undefined,
    withUpdateProgress: async (_message, action) => action(),
    resolveConfiguredFieldName: (noteInfo, preferredName) => {
      if (!preferredName) return null;
      return Object.keys(noteInfo.fields).find((field) => field === preferredName) ?? null;
    },
    resolveNoteFieldName: (noteInfo, preferredName) => {
      if (!preferredName) return null;
      return Object.keys(noteInfo.fields).find((field) => field === preferredName) ?? null;
    },
    getAnimatedImageLeadInSeconds: async () => 0,
    extractFields: () => ({}),
    processSentence: (sentence) => sentence,
    setCardTypeFields: () => undefined,
    mergeFieldValue: (_existing, newValue) => newValue,
    formatMiscInfoPattern: () => '',
    getEffectiveSentenceCardConfig: () => ({
      model: 'Sentence',
      sentenceField: 'Sentence',
      audioField: 'SentenceAudio',
      lapisEnabled: false,
      kikuEnabled: false,
      fieldGroupingMode: 'disabled',
    }),
    getFallbackDurationSeconds: () => 10,
    appendKnownWordsFromNoteInfo: () => undefined,
    removeKnownWordNote: () => undefined,
    isUpdateInProgress: () => false,
    setUpdateInProgress: () => undefined,
    trackLastAddedNoteId: () => undefined,
  });

  const created = await service.createSentenceCard('テスト', 10, 12);

  assert.equal(created, true);
  assert.equal(streamRequests, 0);
  assert.deepEqual(mediaCalls, []);
  assert.deepEqual(queuedUpdates, [
    {
      sourceUrl: 'https://www.youtube.com/watch?v=abc123',
      noteId: 42,
      startTime: 10,
      endTime: 12,
      label: 'テスト',
      audioFieldName: 'SentenceAudio',
      imageFieldName: 'Picture',
      miscInfoFieldName: 'MiscInfo',
      generateAudio: true,
      generateImage: true,
      volumeScale: 0.35 ** 3,
    },
  ]);
  assert.deepEqual(updates, []);
});

test('CardCreationService tracks pre-add duplicate note ids for kiku sentence cards', async () => {
  const trackedDuplicates: Array<{ noteId: number; duplicateNoteIds: number[] }> = [];
  const duplicateLookupExpressions: string[] = [];

  const service = new CardCreationService({
    getConfig: () =>
      ({
        deck: 'Mining',
        fields: {
          word: 'Expression',
          sentence: 'Sentence',
          audio: 'SentenceAudio',
        },
        media: {
          generateAudio: false,
          generateImage: false,
        },
        behavior: {},
        ai: false,
      }) as AnkiConnectConfig,
    getAiConfig: () => ({}),
    getTimingTracker: () => ({}) as never,
    getMpvClient: () =>
      ({
        currentVideoPath: '/video.mp4',
        currentSubText: '字幕',
        currentSubStart: 1,
        currentSubEnd: 2,
        currentTimePos: 1.5,
        currentAudioStreamIndex: 0,
      }) as never,
    client: {
      addNote: async () => 42,
      addTags: async () => undefined,
      notesInfo: async () => [],
      updateNoteFields: async () => undefined,
      storeMediaFile: async () => undefined,
      findNotes: async () => [],
      retrieveMediaFile: async () => '',
      deleteNotes: async () => undefined,
    },
    mediaGenerator: {
      generateAudio: async () => null,
      generateScreenshot: async () => null,
      generateAnimatedImage: async () => null,
    },
    showOsdNotification: () => undefined,
    showUpdateResult: () => undefined,
    showStatusNotification: () => undefined,
    showNotification: async () => undefined,
    beginUpdateProgress: () => undefined,
    endUpdateProgress: () => undefined,
    withUpdateProgress: async (_message, action) => action(),
    resolveConfiguredFieldName: () => null,
    resolveNoteFieldName: () => null,
    getAnimatedImageLeadInSeconds: async () => 0,
    extractFields: () => ({}),
    processSentence: (sentence) => sentence,
    setCardTypeFields: () => undefined,
    mergeFieldValue: (_existing, newValue) => newValue,
    formatMiscInfoPattern: () => '',
    getEffectiveSentenceCardConfig: () => ({
      model: 'Sentence',
      sentenceField: 'Sentence',
      audioField: 'SentenceAudio',
      lapisEnabled: false,
      kikuEnabled: true,
      fieldGroupingMode: 'manual',
    }),
    getFallbackDurationSeconds: () => 10,
    appendKnownWordsFromNoteInfo: () => undefined,
    removeKnownWordNote: () => undefined,
    isUpdateInProgress: () => false,
    setUpdateInProgress: () => undefined,
    trackLastAddedNoteId: () => undefined,
    findDuplicateNoteIds: async (expression) => {
      duplicateLookupExpressions.push(expression);
      return [18, 7, 30, 7];
    },
    trackLastAddedDuplicateNoteIds: (noteId, duplicateNoteIds) => {
      trackedDuplicates.push({ noteId, duplicateNoteIds });
    },
  });

  const created = await service.createSentenceCard('重複文', 0, 1);

  assert.equal(created, true);
  assert.deepEqual(duplicateLookupExpressions, ['重複文']);
  assert.deepEqual(trackedDuplicates, [{ noteId: 42, duplicateNoteIds: [7, 18, 30] }]);
});

test('CardCreationService does not track duplicate ids when pre-add lookup returns none', async () => {
  const trackedDuplicates: Array<{ noteId: number; duplicateNoteIds: number[] }> = [];

  const service = new CardCreationService({
    getConfig: () =>
      ({
        deck: 'Mining',
        fields: {
          word: 'Expression',
          sentence: 'Sentence',
          audio: 'SentenceAudio',
        },
        media: {
          generateAudio: false,
          generateImage: false,
        },
        behavior: {},
        ai: false,
      }) as AnkiConnectConfig,
    getAiConfig: () => ({}),
    getTimingTracker: () => ({}) as never,
    getMpvClient: () =>
      ({
        currentVideoPath: '/video.mp4',
        currentSubText: '字幕',
        currentSubStart: 1,
        currentSubEnd: 2,
        currentTimePos: 1.5,
        currentAudioStreamIndex: 0,
      }) as never,
    client: {
      addNote: async () => 42,
      addTags: async () => undefined,
      notesInfo: async () => [],
      updateNoteFields: async () => undefined,
      storeMediaFile: async () => undefined,
      findNotes: async () => [],
      retrieveMediaFile: async () => '',
      deleteNotes: async () => undefined,
    },
    mediaGenerator: {
      generateAudio: async () => null,
      generateScreenshot: async () => null,
      generateAnimatedImage: async () => null,
    },
    showOsdNotification: () => undefined,
    showUpdateResult: () => undefined,
    showStatusNotification: () => undefined,
    showNotification: async () => undefined,
    beginUpdateProgress: () => undefined,
    endUpdateProgress: () => undefined,
    withUpdateProgress: async (_message, action) => action(),
    resolveConfiguredFieldName: () => null,
    resolveNoteFieldName: () => null,
    getAnimatedImageLeadInSeconds: async () => 0,
    extractFields: () => ({}),
    processSentence: (sentence) => sentence,
    setCardTypeFields: () => undefined,
    mergeFieldValue: (_existing, newValue) => newValue,
    formatMiscInfoPattern: () => '',
    getEffectiveSentenceCardConfig: () => ({
      model: 'Sentence',
      sentenceField: 'Sentence',
      audioField: 'SentenceAudio',
      lapisEnabled: false,
      kikuEnabled: true,
      fieldGroupingMode: 'manual',
    }),
    getFallbackDurationSeconds: () => 10,
    appendKnownWordsFromNoteInfo: () => undefined,
    removeKnownWordNote: () => undefined,
    isUpdateInProgress: () => false,
    setUpdateInProgress: () => undefined,
    trackLastAddedNoteId: () => undefined,
    findDuplicateNoteIds: async () => [],
    trackLastAddedDuplicateNoteIds: (noteId, duplicateNoteIds) => {
      trackedDuplicates.push({ noteId, duplicateNoteIds });
    },
  });

  const created = await service.createSentenceCard('重複なし', 0, 1);

  assert.equal(created, true);
  assert.deepEqual(trackedDuplicates, []);
});
