import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

import type { AnkiConnectConfig } from '../types/anki';
import { KnownWordCacheManager } from './known-word-cache';

async function waitForCondition(
  condition: () => boolean,
  timeoutMs = 500,
  intervalMs = 10,
): Promise<void> {
  const startedAt = Date.now();
  while (Date.now() - startedAt < timeoutMs) {
    if (condition()) {
      return;
    }
    await new Promise((resolve) => setTimeout(resolve, intervalMs));
  }
  throw new Error('Timed out waiting for condition');
}

function createKnownWordCacheHarness(config: AnkiConnectConfig): {
  manager: KnownWordCacheManager;
  calls: {
    findNotes: number;
    notesInfo: number;
  };
  statePath: string;
  clientState: {
    findNotesResult: number[];
    notesInfoResult: Array<{ noteId: number; fields: Record<string, { value: string }> }>;
    findNotesByQuery: Map<string, number[]>;
  };
  cleanup: () => void;
} {
  const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-known-word-cache-'));
  const statePath = path.join(stateDir, 'known-words-cache.json');
  const calls = {
    findNotes: 0,
    notesInfo: 0,
  };
  const clientState = {
    findNotesResult: [] as number[],
    notesInfoResult: [] as Array<{ noteId: number; fields: Record<string, { value: string }> }>,
    findNotesByQuery: new Map<string, number[]>(),
  };
  const manager = new KnownWordCacheManager({
    client: {
      findNotes: async (query) => {
        calls.findNotes += 1;
        if (clientState.findNotesByQuery.has(query)) {
          return clientState.findNotesByQuery.get(query) ?? [];
        }
        return clientState.findNotesResult;
      },
      notesInfo: async (noteIds) => {
        calls.notesInfo += 1;
        return clientState.notesInfoResult.filter((note) => noteIds.includes(note.noteId));
      },
    },
    getConfig: () => config,
    knownWordCacheStatePath: statePath,
    showStatusNotification: () => undefined,
  });

  return {
    manager,
    calls,
    statePath,
    clientState,
    cleanup: () => {
      fs.rmSync(stateDir, { recursive: true, force: true });
    },
  };
}

test('KnownWordCacheManager startLifecycle keeps fresh persisted cache without immediate refresh', async () => {
  const config: AnkiConnectConfig = {
    knownWords: {
      highlightEnabled: true,
      refreshMinutes: 60,
    },
  };
  const { manager, calls, statePath, cleanup } = createKnownWordCacheHarness(config);
  const originalDateNow = Date.now;

  try {
    Date.now = () => 120_000;
    fs.writeFileSync(
      statePath,
      JSON.stringify({
        version: 2,
        refreshedAtMs: 120_000,
        scope: '{"refreshMinutes":60,"scope":"all","fieldsWord":""}',
        words: ['猫'],
        notes: {
          '1': ['猫'],
        },
      }),
      'utf-8',
    );

    manager.startLifecycle();

    assert.equal(manager.isKnownWord('猫'), true);
    assert.equal(calls.findNotes, 0);
    assert.equal(calls.notesInfo, 0);
    // v2 states carry no readings, so they load usable but stale to trigger a
    // prompt upgrade refresh.
    assert.equal(
      (
        manager as unknown as {
          getMsUntilNextRefresh: () => number;
        }
      ).getMsUntilNextRefresh(),
      0,
    );
  } finally {
    Date.now = originalDateNow;
    manager.stopLifecycle();
    cleanup();
  }
});

test('KnownWordCacheManager startLifecycle keeps fresh v3 persisted cache without immediate refresh', async () => {
  const config: AnkiConnectConfig = {
    knownWords: {
      highlightEnabled: true,
      refreshMinutes: 60,
    },
  };
  const { manager, calls, statePath, cleanup } = createKnownWordCacheHarness(config);
  const originalDateNow = Date.now;

  try {
    Date.now = () => 120_000;
    fs.writeFileSync(
      statePath,
      JSON.stringify({
        version: 3,
        refreshedAtMs: 120_000,
        scope: '{"refreshMinutes":60,"scope":"all","fieldsWord":""}',
        notes: {
          '1': [{ word: '猫', reading: 'ねこ' }],
        },
      }),
      'utf-8',
    );

    manager.startLifecycle();

    assert.equal(manager.isKnownWord('猫'), true);
    assert.equal(manager.isKnownWord('猫', 'ねこ'), true);
    assert.equal(manager.isKnownWord('猫', 'びょう'), false);
    assert.equal(calls.findNotes, 0);
    assert.equal(calls.notesInfo, 0);
    assert.equal(
      (
        manager as unknown as {
          getMsUntilNextRefresh: () => number;
        }
      ).getMsUntilNextRefresh() > 0,
      true,
    );
  } finally {
    Date.now = originalDateNow;
    manager.stopLifecycle();
    cleanup();
  }
});

test('KnownWordCacheManager startLifecycle immediately refreshes stale persisted cache', async () => {
  const config: AnkiConnectConfig = {
    fields: {
      word: 'Word',
    },
    knownWords: {
      highlightEnabled: true,
      refreshMinutes: 1,
    },
  };
  const { manager, calls, statePath, clientState, cleanup } = createKnownWordCacheHarness(config);
  const originalDateNow = Date.now;

  try {
    Date.now = () => 120_000;
    fs.writeFileSync(
      statePath,
      JSON.stringify({
        version: 2,
        refreshedAtMs: 59_000,
        scope: '{"refreshMinutes":1,"scope":"all","fieldsWord":"Word"}',
        words: ['猫'],
        notes: {
          '1': ['猫'],
        },
      }),
      'utf-8',
    );

    clientState.findNotesResult = [1];
    clientState.notesInfoResult = [
      {
        noteId: 1,
        fields: {
          Word: { value: '犬' },
        },
      },
    ];

    manager.startLifecycle();
    await waitForCondition(() => calls.findNotes === 1 && calls.notesInfo === 1);

    assert.equal(manager.isKnownWord('猫'), false);
    assert.equal(manager.isKnownWord('犬'), true);
  } finally {
    Date.now = originalDateNow;
    manager.stopLifecycle();
    cleanup();
  }
});

test('KnownWordCacheManager invalidates persisted cache when fields.word changes', () => {
  const config: AnkiConnectConfig = {
    deck: 'Mining',
    fields: {
      word: 'Word',
    },
    knownWords: {
      highlightEnabled: true,
    },
  };
  const { manager, cleanup } = createKnownWordCacheHarness(config);

  try {
    manager.appendFromNoteInfo({
      noteId: 1,
      fields: {
        Word: { value: '猫' },
      },
    });
    assert.equal(manager.isKnownWord('猫'), true);

    config.fields = {
      ...config.fields,
      word: 'Expression',
    };

    (
      manager as unknown as {
        loadKnownWordCacheState: () => void;
      }
    ).loadKnownWordCacheState();

    assert.equal(manager.isKnownWord('猫'), false);
  } finally {
    cleanup();
  }
});

test('KnownWordCacheManager removes a deleted note from memory and persisted state', () => {
  const config: AnkiConnectConfig = {
    fields: { word: 'Word' },
    knownWords: { highlightEnabled: true },
  };
  const { manager, statePath, cleanup } = createKnownWordCacheHarness(config);

  try {
    manager.appendFromNoteInfo({
      noteId: 42,
      fields: { Word: { value: '猫' } },
    });

    assert.equal(manager.removeNote(42), true);
    assert.equal(manager.removeNote(42), false);
    assert.equal(manager.isKnownWord('猫'), false);

    const persisted = JSON.parse(fs.readFileSync(statePath, 'utf-8')) as {
      notes?: Record<string, unknown>;
    };
    assert.deepEqual(persisted.notes, {});
  } finally {
    cleanup();
  }
});

test('KnownWordCacheManager refresh incrementally reconciles deleted and edited note words', async () => {
  const config: AnkiConnectConfig = {
    fields: {
      word: 'Word',
    },
    knownWords: {
      highlightEnabled: true,
    },
  };
  const { manager, statePath, clientState, cleanup } = createKnownWordCacheHarness(config);

  try {
    fs.writeFileSync(
      statePath,
      JSON.stringify({
        version: 2,
        refreshedAtMs: 1,
        scope: '{"refreshMinutes":1440,"scope":"all","fieldsWord":"Word"}',
        words: ['猫', '犬'],
        notes: {
          '1': ['猫'],
          '2': ['犬'],
        },
      }),
      'utf-8',
    );

    (
      manager as unknown as {
        loadKnownWordCacheState: () => void;
      }
    ).loadKnownWordCacheState();

    clientState.findNotesResult = [1];
    clientState.notesInfoResult = [
      {
        noteId: 1,
        fields: {
          Word: { value: '鳥' },
        },
      },
    ];

    await manager.refresh(true);

    assert.equal(manager.isKnownWord('猫'), false);
    assert.equal(manager.isKnownWord('犬'), false);
    assert.equal(manager.isKnownWord('鳥'), true);

    const persisted = JSON.parse(fs.readFileSync(statePath, 'utf-8')) as {
      version: number;
      notes?: Record<string, Array<{ word: string; reading: string | null }>>;
    };
    assert.equal(persisted.version, 4);
    assert.deepEqual(persisted.notes, {
      '1': [{ word: '鳥', reading: null }],
    });
  } finally {
    cleanup();
  }
});

test('KnownWordCacheManager uses empty query when no known-word deck is configured', async () => {
  const config: AnkiConnectConfig = {
    fields: {
      word: 'Word',
    },
    knownWords: {
      highlightEnabled: true,
    },
  };
  const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);

  try {
    clientState.findNotesByQuery.set('', [1]);
    clientState.notesInfoResult = [
      {
        noteId: 1,
        fields: {
          Word: { value: '猫' },
        },
      },
    ];

    await manager.refresh(true);

    assert.equal(manager.isKnownWord('猫'), true);
  } finally {
    cleanup();
  }
});

test('KnownWordCacheManager skips malformed note info without fields', async () => {
  const config: AnkiConnectConfig = {
    fields: {
      word: 'Word',
    },
    knownWords: {
      highlightEnabled: true,
    },
  };
  const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);

  try {
    clientState.findNotesResult = [1, 2];
    clientState.notesInfoResult = [
      {
        noteId: 1,
        fields: undefined as unknown as Record<string, { value: string }>,
      },
      {
        noteId: 2,
        fields: {
          Word: { value: '猫' },
        },
      },
    ];

    await manager.refresh(true);

    assert.equal(manager.isKnownWord('猫'), true);
    assert.equal(manager.isKnownWord('犬'), false);
  } finally {
    cleanup();
  }
});

test('KnownWordCacheManager preserves cache state key captured before refresh work', async () => {
  const config: AnkiConnectConfig = {
    fields: {
      word: 'Word',
    },
    knownWords: {
      highlightEnabled: true,
      refreshMinutes: 1,
    },
  };
  const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-known-word-cache-key-'));
  const statePath = path.join(stateDir, 'known-words-cache.json');
  let notesInfoStarted = false;
  let releaseNotesInfo!: () => void;
  const notesInfoGate = new Promise<void>((resolve) => {
    releaseNotesInfo = resolve;
  });
  const manager = new KnownWordCacheManager({
    client: {
      findNotes: async () => [1],
      notesInfo: async () => {
        notesInfoStarted = true;
        await notesInfoGate;
        return [
          {
            noteId: 1,
            fields: {
              Word: { value: '猫' },
            },
          },
        ];
      },
    },
    getConfig: () => config,
    knownWordCacheStatePath: statePath,
    showStatusNotification: () => undefined,
  });

  try {
    const refreshPromise = manager.refresh(true);
    await waitForCondition(() => notesInfoStarted);

    config.fields = {
      ...config.fields,
      word: 'Expression',
    };
    releaseNotesInfo();
    await refreshPromise;

    const persisted = JSON.parse(fs.readFileSync(statePath, 'utf-8')) as {
      scope: string;
      notes: Record<string, Array<{ word: string; reading: string | null }>>;
    };
    assert.equal(persisted.scope, '{"refreshMinutes":1,"scope":"all","fieldsWord":"Word"}');
    assert.deepEqual(persisted.notes, { '1': [{ word: '猫', reading: null }] });
  } finally {
    fs.rmSync(stateDir, { recursive: true, force: true });
  }
});

test('KnownWordCacheManager does not borrow fields from other decks during refresh', async () => {
  const config: AnkiConnectConfig = {
    knownWords: {
      highlightEnabled: true,
      decks: {
        Mining: [],
        Reading: ['AltWord'],
      },
    },
  };
  const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);

  try {
    clientState.findNotesByQuery.set('deck:"Mining"', [1]);
    clientState.findNotesByQuery.set('deck:"Reading"', []);
    clientState.notesInfoResult = [
      {
        noteId: 1,
        fields: {
          AltWord: { value: '猫' },
        },
      },
    ];

    await manager.refresh(true);

    assert.equal(manager.isKnownWord('猫'), false);
  } finally {
    cleanup();
  }
});

test('KnownWordCacheManager invalidates persisted cache when per-deck fields change', () => {
  const config: AnkiConnectConfig = {
    fields: {
      word: 'Word',
    },
    knownWords: {
      highlightEnabled: true,
      decks: {
        Mining: ['Word'],
      },
    },
  };
  const { manager, cleanup } = createKnownWordCacheHarness(config);

  try {
    manager.appendFromNoteInfo({
      noteId: 1,
      fields: {
        Word: { value: '猫' },
      },
    });
    assert.equal(manager.isKnownWord('猫'), true);

    config.knownWords = {
      ...config.knownWords,
      decks: {
        Mining: ['Expression'],
      },
    };

    (
      manager as unknown as {
        loadKnownWordCacheState: () => void;
      }
    ).loadKnownWordCacheState();

    assert.equal(manager.isKnownWord('猫'), false);
  } finally {
    cleanup();
  }
});

test('KnownWordCacheManager preserves deck-specific field mappings during refresh', async () => {
  const config: AnkiConnectConfig = {
    knownWords: {
      highlightEnabled: true,
      decks: {
        Mining: ['Expression'],
        Reading: ['Word'],
      },
    },
  };
  const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);

  try {
    clientState.findNotesByQuery.set('deck:"Mining"', [1]);
    clientState.findNotesByQuery.set('deck:"Reading"', [2]);
    clientState.notesInfoResult = [
      {
        noteId: 1,
        fields: {
          Expression: { value: '猫' },
          Word: { value: 'should-not-count' },
        },
      },
      {
        noteId: 2,
        fields: {
          Word: { value: '犬' },
          Expression: { value: 'also-ignored' },
        },
      },
    ];

    await manager.refresh(true);

    assert.equal(manager.isKnownWord('猫'), true);
    assert.equal(manager.isKnownWord('犬'), true);
    assert.equal(manager.isKnownWord('should-not-count'), false);
    assert.equal(manager.isKnownWord('also-ignored'), false);
  } finally {
    cleanup();
  }
});

test('KnownWordCacheManager uses the current deck fields for immediate append', () => {
  const config: AnkiConnectConfig = {
    deck: 'Mining',
    fields: {
      word: 'Word',
    },
    knownWords: {
      highlightEnabled: true,
      decks: {
        Mining: ['Expression'],
        Reading: ['Word'],
      },
    },
  };
  const { manager, cleanup } = createKnownWordCacheHarness(config);

  try {
    manager.appendFromNoteInfo({
      noteId: 1,
      fields: {
        Expression: { value: '猫' },
        Word: { value: 'should-not-count' },
      },
    });

    assert.equal(manager.isKnownWord('猫'), true);
    assert.equal(manager.isKnownWord('should-not-count'), false);
  } finally {
    cleanup();
  }
});

test('KnownWordCacheManager uses all configured deck fields for immediate append without a current deck', () => {
  const config: AnkiConnectConfig = {
    deck: '',
    fields: {
      word: 'Expression',
    },
    knownWords: {
      highlightEnabled: true,
      decks: {
        'Kaishi 1.5k': ['Word'],
        Minecraft: ['Expression', 'Word'],
      },
    },
  };
  const { manager, cleanup } = createKnownWordCacheHarness(config);

  try {
    manager.appendFromNoteInfo({
      noteId: 1,
      fields: {
        Expression: { value: '別人' },
      },
    });

    assert.equal(manager.isKnownWord('別人'), true);
  } finally {
    cleanup();
  }
});

test('KnownWordCacheManager reports immediate append cache clears as mutations', () => {
  const config: AnkiConnectConfig = {
    fields: {
      word: 'Expression',
    },
    knownWords: {
      highlightEnabled: true,
      refreshMinutes: 60,
    },
  };
  const { manager, statePath, cleanup } = createKnownWordCacheHarness(config);

  try {
    fs.writeFileSync(
      statePath,
      JSON.stringify({
        version: 2,
        refreshedAtMs: Date.now(),
        scope: '{"refreshMinutes":60,"scope":"all","fieldsWord":"Expression"}',
        words: ['猫'],
        notes: {
          '1': ['猫'],
        },
      }),
      'utf-8',
    );
    manager.startLifecycle();
    assert.equal(manager.isKnownWord('猫'), true);

    config.fields = { word: 'Word' };
    const changed = manager.appendFromNoteInfo({
      noteId: 2,
      fields: {
        Word: { value: '' },
      },
    });

    assert.equal(changed, true);
    assert.equal(manager.isKnownWord('猫'), false);
  } finally {
    manager.stopLifecycle();
    cleanup();
  }
});

test('KnownWordCacheManager skips immediate append when addMinedWordsImmediately is disabled', () => {
  const config: AnkiConnectConfig = {
    knownWords: {
      highlightEnabled: true,
      addMinedWordsImmediately: false,
    },
  };
  const { manager, statePath, cleanup } = createKnownWordCacheHarness(config);

  try {
    manager.appendFromNoteInfo({
      noteId: 1,
      fields: {
        Expression: { value: '猫' },
      },
    });

    assert.equal(manager.isKnownWord('猫'), false);
    assert.equal(fs.existsSync(statePath), false);
  } finally {
    cleanup();
  }
});

test('KnownWordCacheManager disambiguates known words by note reading', async () => {
  const config: AnkiConnectConfig = {
    fields: {
      word: 'Word',
    },
    knownWords: {
      highlightEnabled: true,
    },
  };
  const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);

  try {
    clientState.findNotesResult = [1];
    clientState.notesInfoResult = [
      {
        noteId: 1,
        fields: {
          Word: { value: '床' },
          'Word Reading': { value: 'ゆか' },
        },
      },
    ];

    await manager.refresh(true);

    assert.equal(manager.isKnownWord('床'), true);
    assert.equal(manager.isKnownWord('床', 'ゆか'), true);
    assert.equal(manager.isKnownWord('床', 'ユカ'), true);
    // Same spelling, different word (床/とこ "bed") must not match.
    assert.equal(manager.isKnownWord('床', 'とこ'), false);
    // Note readings stay matchable as kana words.
    assert.equal(manager.isKnownWord('ゆか'), true);
    assert.equal(manager.isKnownWord('とこ'), false);
  } finally {
    cleanup();
  }
});

test('KnownWordCacheManager suppresses reading-only matches when disallowed', async () => {
  const config: AnkiConnectConfig = {
    fields: {
      word: 'Word',
    },
    knownWords: {
      highlightEnabled: true,
    },
  };
  const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);

  try {
    clientState.findNotesResult = [1];
    clientState.notesInfoResult = [
      {
        noteId: 1,
        fields: {
          Word: { value: '警告' },
          'Word Reading': { value: 'けいこく' },
        },
      },
    ];

    await manager.refresh(true);

    // Reading-only match stays available for kana subtitle text…
    assert.equal(manager.isKnownWord('けいこく'), true);
    // …but a kanji token's reading (渓谷/けいこく) must not borrow 警告's.
    assert.equal(
      manager.isKnownWord('けいこく', undefined, { allowReadingOnlyMatch: false }),
      false,
    );
    // Mined word texts still match regardless of the flag.
    assert.equal(manager.isKnownWord('警告', undefined, { allowReadingOnlyMatch: false }), true);
  } finally {
    cleanup();
  }
});

test('KnownWordCacheManager does not match single-kana text by reading alone', async () => {
  const config: AnkiConnectConfig = {
    fields: {
      word: 'Word',
    },
    knownWords: {
      highlightEnabled: true,
    },
  };
  const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);

  try {
    clientState.findNotesResult = [1, 2];
    clientState.notesInfoResult = [
      {
        noteId: 1,
        fields: {
          Word: { value: '夜' },
          'Word Reading': { value: 'よ' },
        },
      },
      {
        noteId: 2,
        fields: {
          Word: { value: 'え' },
        },
      },
    ];

    await manager.refresh(true);

    // よ must not count as known just because 夜 is read よ.
    assert.equal(manager.isKnownWord('よ'), false);
    assert.equal(manager.isKnownWord('ヨ'), false);
    assert.equal(manager.isKnownWord('夜'), true);
    assert.equal(manager.isKnownWord('夜', 'よ'), true);
    // A literal single-kana word entry still matches via the word map.
    assert.equal(manager.isKnownWord('え'), true);
  } finally {
    cleanup();
  }
});

test('KnownWordCacheManager probes reading fields even with per-deck word fields configured', async () => {
  const config: AnkiConnectConfig = {
    fields: {
      word: 'Expression',
    },
    knownWords: {
      highlightEnabled: true,
      decks: {
        'Kaishi 1.5k': ['Word'],
      },
    },
  };
  const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);

  try {
    clientState.findNotesByQuery.set('deck:"Kaishi 1.5k"', [1]);
    clientState.notesInfoResult = [
      {
        noteId: 1,
        fields: {
          Word: { value: '床' },
          'Word Reading': { value: 'ゆか' },
        },
      },
    ];

    await manager.refresh(true);

    assert.equal(manager.isKnownWord('床', 'ゆか'), true);
    assert.equal(manager.isKnownWord('床', 'とこ'), false);
  } finally {
    cleanup();
  }
});

test('KnownWordCacheManager matches words without readings in any reading', async () => {
  const config: AnkiConnectConfig = {
    fields: {
      word: 'Word',
    },
    knownWords: {
      highlightEnabled: true,
    },
  };
  const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);

  try {
    clientState.findNotesResult = [1];
    clientState.notesInfoResult = [
      {
        noteId: 1,
        fields: {
          Word: { value: '床' },
        },
      },
    ];

    await manager.refresh(true);

    assert.equal(manager.isKnownWord('床'), true);
    assert.equal(manager.isKnownWord('床', 'とこ'), true);
  } finally {
    cleanup();
  }
});

test('KnownWordCacheManager extracts word and reading from furigana word fields', async () => {
  const config: AnkiConnectConfig = {
    fields: {
      word: 'Word',
    },
    knownWords: {
      highlightEnabled: true,
    },
  };
  const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);

  try {
    clientState.findNotesResult = [1];
    clientState.notesInfoResult = [
      {
        noteId: 1,
        fields: {
          Word: { value: 'お 決[き]まり' },
        },
      },
    ];

    await manager.refresh(true);

    assert.equal(manager.isKnownWord('お決まり'), true);
    assert.equal(manager.isKnownWord('お決まり', 'おきまり'), true);
    assert.equal(manager.isKnownWord('お決まり', 'おさだまり'), false);
  } finally {
    cleanup();
  }
});

test('KnownWordCacheManager treats non-kana reading fields as words', async () => {
  const config: AnkiConnectConfig = {
    fields: {
      word: 'Word',
    },
    knownWords: {
      highlightEnabled: true,
    },
  };
  const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);

  try {
    clientState.findNotesResult = [1];
    clientState.notesInfoResult = [
      {
        noteId: 1,
        fields: {
          Reading: { value: '漢字' },
        },
      },
    ];

    await manager.refresh(true);

    assert.equal(manager.isKnownWord('漢字'), true);
  } finally {
    cleanup();
  }
});

test('KnownWordCacheManager keeps kana-only reading notes matchable', async () => {
  const config: AnkiConnectConfig = {
    fields: {
      word: 'Word',
    },
    knownWords: {
      highlightEnabled: true,
    },
  };
  const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);

  try {
    clientState.findNotesResult = [1];
    clientState.notesInfoResult = [
      {
        noteId: 1,
        fields: {
          Reading: { value: 'たべる' },
        },
      },
    ];

    await manager.refresh(true);

    assert.equal(manager.isKnownWord('たべる'), true);
    assert.equal(manager.isKnownWord('タベル'), true);
  } finally {
    cleanup();
  }
});
