// Contract tests for the storage layer, run against the web (Dexie) repo.
//
// fake-indexeddb/auto polyfills IndexedDB into the Node global so Dexie works
// under vitest. We test the Dexie repo as the reference implementation of the
// StorageRepo contract; the native expo-sqlite repo (native-only) is NOT
// imported here because expo-sqlite cannot load outside a device/simulator.
import 'fake-indexeddb/auto';

import { describe, it, expect, beforeEach } from 'vitest';
import { repo } from './repo.web';
import { parseDraft, type TranscriptDraft } from './schema';

// Helper: a minimal valid draft, overridable per test.
function makeDraft(over: Partial<TranscriptDraft> = {}): TranscriptDraft {
  return {
    title: 'My Recording',
    durationSec: 12.5,
    modelId: 'tiny.en',
    segments: [
      { start: 0, end: 2, text: 'hello world' },
      { start: 2, end: 5, text: 'second segment', confidence: 0.9 },
    ],
    ...over,
  };
}

// Wipe storage between tests so ordering/search assertions are deterministic.
beforeEach(async () => {
  const all = await repo.list();
  await Promise.all(all.map((m) => repo.remove(m.id)));
});

describe('StorageRepo (Dexie web impl)', () => {
  it('create -> get round-trips segments and metadata', async () => {
    const created = await repo.create(makeDraft());

    expect(created.id).toMatch(/^t_/);
    expect(created.createdAt).toBeTypeOf('number');
    expect(created.updatedAt).toBe(created.createdAt);
    expect(created.segmentCount).toBe(2);

    const fetched = await repo.get(created.id);
    expect(fetched).toBeDefined();
    expect(fetched!.title).toBe('My Recording');
    expect(fetched!.durationSec).toBe(12.5);
    expect(fetched!.modelId).toBe('tiny.en');
    // Segments survive the round-trip exactly.
    expect(fetched!.segments).toEqual([
      { start: 0, end: 2, text: 'hello world' },
      { start: 2, end: 5, text: 'second segment', confidence: 0.9 },
    ]);
  });

  it('get returns undefined for an unknown id', async () => {
    expect(await repo.get('t_nope')).toBeUndefined();
  });

  it('list returns metadatas newest-first (without segments)', async () => {
    const a = await repo.create(makeDraft({ title: 'first' }));
    // Force distinct, increasing createdAt timestamps.
    await new Promise((r) => setTimeout(r, 2));
    const b = await repo.create(makeDraft({ title: 'second' }));
    await new Promise((r) => setTimeout(r, 2));
    const c = await repo.create(makeDraft({ title: 'third' }));

    const metas = await repo.list();
    expect(metas.map((m) => m.id)).toEqual([c.id, b.id, a.id]);
    // Metas omit the segment body.
    expect((metas[0] as unknown as Record<string, unknown>).segments).toBeUndefined();
    expect(metas[0]!.segmentCount).toBe(2);
  });

  it('update changes title + segments and re-derives searchText for search', async () => {
    const created = await repo.create(
      makeDraft({ title: 'Old Title', segments: [{ start: 0, end: 1, text: 'apple' }] }),
    );
    const originalUpdatedAt = created.updatedAt;
    await new Promise((r) => setTimeout(r, 2));

    const updated = await repo.update(created.id, {
      title: 'Brand New Title',
      segments: [
        { start: 0, end: 1, text: 'banana' },
        { start: 1, end: 2, text: 'cherry' },
      ],
    });

    expect(updated.title).toBe('Brand New Title');
    expect(updated.segmentCount).toBe(2);
    expect(updated.updatedAt).toBeGreaterThan(originalUpdatedAt);

    // Old text is no longer findable...
    expect(await repo.search('apple')).toHaveLength(0);
    // ...and the new text + new title are.
    const byText = await repo.search('cherry');
    expect(byText.map((m) => m.id)).toEqual([created.id]);
    const byTitle = await repo.search('brand new');
    expect(byTitle.map((m) => m.id)).toEqual([created.id]);
  });

  it('update throws for an unknown id', async () => {
    await expect(repo.update('t_missing', { title: 'x' })).rejects.toThrow();
  });

  it('search matches title and segment text case-insensitively', async () => {
    const t = await repo.create(
      makeDraft({
        title: 'Quarterly MEETING notes',
        segments: [{ start: 0, end: 3, text: 'Discuss the BUDGET forecast' }],
      }),
    );

    // Title match, different case.
    expect((await repo.search('meeting')).map((m) => m.id)).toEqual([t.id]);
    // Segment-text match, different case.
    expect((await repo.search('budget')).map((m) => m.id)).toEqual([t.id]);
    // Non-match.
    expect(await repo.search('zzz-not-present')).toHaveLength(0);
  });

  it('remove deletes a transcript', async () => {
    const created = await repo.create(makeDraft());
    expect(await repo.get(created.id)).toBeDefined();

    await repo.remove(created.id);
    expect(await repo.get(created.id)).toBeUndefined();
    expect(await repo.list()).toHaveLength(0);
  });

  it('parseDraft rejects a NaN duration', () => {
    expect(() => parseDraft(makeDraft({ durationSec: NaN }))).toThrow();
    expect(() => parseDraft(makeDraft({ durationSec: Infinity }))).toThrow();
  });

  it('parseDraft rejects a non-finite segment time', () => {
    expect(() =>
      parseDraft(makeDraft({ segments: [{ start: 0, end: Infinity, text: 'x' }] })),
    ).toThrow();
    expect(() =>
      parseDraft(makeDraft({ segments: [{ start: NaN, end: 1, text: 'x' }] })),
    ).toThrow();
  });

  it('create rejects an invalid draft (defense in depth)', async () => {
    // create() calls parseDraft internally, so bad input never persists.
    await expect(
      repo.create(makeDraft({ durationSec: -1 })),
    ).rejects.toThrow();
  });
});
