import { expect, test } from '@playwright/test';
import {
  compareManifestSegments,
  decodeManifestCursor,
  dedupeManifestVariants,
  encodeManifestCursor,
  parseManifestPageSize,
} from '../../src/lib/server/tts/segments-manifest';

test.describe('tts segments manifest helpers', () => {
  test('dedupe prefers completed variant over newer pending for same settings key', () => {
    const variants = dedupeManifestVariants([
      {
        dedupeKey: 'settings:abc',
        variant: {
          segmentId: 'old-completed',
          settings: null,
          audioPresignUrl: '/audio/old',
          audioFallbackUrl: '/audio/old/fallback',
          durationMs: 1100,
          status: 'completed',
          textLength: 12,
          alignmentWordCount: 2,
          audioKey: 'old',
          updatedAt: 100,
        },
      },
      {
        dedupeKey: 'settings:abc',
        variant: {
          segmentId: 'new-pending',
          settings: null,
          audioPresignUrl: null,
          audioFallbackUrl: null,
          durationMs: null,
          status: 'pending',
          textLength: 15,
          alignmentWordCount: 0,
          audioKey: null,
          updatedAt: 200,
        },
      },
    ]);

    expect(variants).toHaveLength(1);
    expect(variants[0].segmentId).toBe('old-completed');
    expect(variants[0].status).toBe('completed');
  });

  test('dedupe uses status rank as tiebreaker when updatedAt is equal', () => {
    const variants = dedupeManifestVariants([
      {
        dedupeKey: 'settings:abc',
        variant: {
          segmentId: 'pending',
          settings: null,
          audioPresignUrl: null,
          audioFallbackUrl: null,
          durationMs: null,
          status: 'pending',
          textLength: 12,
          alignmentWordCount: 0,
          audioKey: null,
          updatedAt: 123,
        },
      },
      {
        dedupeKey: 'settings:abc',
        variant: {
          segmentId: 'completed',
          settings: null,
          audioPresignUrl: '/audio/new',
          audioFallbackUrl: '/audio/new/fallback',
          durationMs: 1200,
          status: 'completed',
          textLength: 12,
          alignmentWordCount: 2,
          audioKey: 'new',
          updatedAt: 123,
        },
      },
    ]);

    expect(variants).toHaveLength(1);
    expect(variants[0].segmentId).toBe('completed');
    expect(variants[0].status).toBe('completed');
  });

  test('sorts PDF segments deterministically by page and segment index', () => {
    const rows = [
      { groupKey: 'p2-i4', segmentIndex: 4, locator: { readerType: 'pdf' as const, page: 2 } },
      { groupKey: 'p1-i1', segmentIndex: 1, locator: { readerType: 'pdf' as const, page: 1 } },
      { groupKey: 'p1-i0', segmentIndex: 0, locator: { readerType: 'pdf' as const, page: 1 } },
      { groupKey: 'p1-i2', segmentIndex: 2, locator: { readerType: 'pdf' as const, page: 1 } },
    ];

    const sorted = rows.sort(compareManifestSegments);
    expect(sorted.map((row) => row.groupKey)).toEqual(['p1-i0', 'p1-i1', 'p1-i2', 'p2-i4']);
  });

  test('sorts EPUB locators by spineIndex then charOffset (numeric, viewport-stable)', () => {
    // The order below is intentionally jumbled, with spineIndex 10 placed
    // first so that a lexicographic compare on the spine string would put
    // "10" before "2". A correct numeric compare must surface spine 2 first.
    const rows = [
      { groupKey: 'spine-10-off-0', segmentIndex: 0, locator: { readerType: 'epub' as const, spineHref: 'OEBPS/ch10.xhtml', spineIndex: 10, charOffset: 0 } },
      { groupKey: 'spine-2-off-200', segmentIndex: 1, locator: { readerType: 'epub' as const, spineHref: 'OEBPS/ch02.xhtml', spineIndex: 2, charOffset: 200 } },
      { groupKey: 'spine-2-off-50', segmentIndex: 0, locator: { readerType: 'epub' as const, spineHref: 'OEBPS/ch02.xhtml', spineIndex: 2, charOffset: 50 } },
    ];

    const sorted = rows.sort(compareManifestSegments);
    expect(sorted.map((row) => row.groupKey)).toEqual(['spine-2-off-50', 'spine-2-off-200', 'spine-10-off-0']);
  });

  test('groups EPUB rows by spine identity, not by raw CFI', () => {
    // Two rows in the same spine item but at different char offsets must share
    // a groupKey (so the sidebar buckets them together as one chapter), while
    // a row in a different spine sits in its own group.
    const rows = [
      { groupKey: 'a', segmentIndex: 0, locator: { readerType: 'epub' as const, spineHref: 'OEBPS/ch02.xhtml', spineIndex: 2, charOffset: 0, cfi: 'epubcfi(/6/4!/2:0)' } },
      { groupKey: 'b', segmentIndex: 1, locator: { readerType: 'epub' as const, spineHref: 'OEBPS/ch02.xhtml', spineIndex: 2, charOffset: 1024, cfi: 'epubcfi(/6/4!/4:0)' } },
      { groupKey: 'c', segmentIndex: 0, locator: { readerType: 'epub' as const, spineHref: 'OEBPS/ch03.xhtml', spineIndex: 3, charOffset: 0, cfi: 'epubcfi(/6/6!/2:0)' } },
    ];

    const sorted = rows.sort(compareManifestSegments);
    // Recompute groupKey using the production helper to assert the actual
    // grouping behavior rather than the test's hand-labeled groupKey.
    // eslint-disable-next-line @typescript-eslint/no-require-imports
    const { locatorGroupKey } = require('../../src/lib/shared/tts-locator');
    const groupKeys = sorted.map((row) => locatorGroupKey(row.locator));
    expect(groupKeys[0]).toBe(groupKeys[1]); // both ch02
    expect(groupKeys[2]).not.toBe(groupKeys[0]); // ch03 is its own group
  });

  test('manifest aggregator distinguishes rows by identity (regression: per-page rows collapsed by chapter bucket)', () => {
    // Bug reproduction: previously the server-side aggregator keyed rows by
    // `${segmentIndex}|${locatorGroupKey(locator)}`. Because the new EPUB
    // groupKey is chapter-coarse, two persisted rows in different *pages* of
    // the same chapter that happened to share segmentIndex (which is
    // page-relative) collapsed into a single entry — making earlier pages'
    // rows visually "disappear" when later pages were generated.
    //
    // The fix uses `locatorIdentityKey` which includes charOffset for EPUB.
    // eslint-disable-next-line @typescript-eslint/no-require-imports
    const { locatorIdentityKey, locatorGroupKey } = require('../../src/lib/shared/tts-locator');

    const page1Row0 = { readerType: 'epub' as const, spineHref: 'OEBPS/ch02.xhtml', spineIndex: 2, charOffset: 0 };
    const page2Row0 = { readerType: 'epub' as const, spineHref: 'OEBPS/ch02.xhtml', spineIndex: 2, charOffset: 2048 };

    // Same chapter bucket (sidebar grouping should put them under one chapter header).
    expect(locatorGroupKey(page1Row0)).toBe(locatorGroupKey(page2Row0));
    // Different storage identity (server aggregator must NOT collapse them).
    const segmentIndex = 0;
    const k1 = `${segmentIndex}|${locatorIdentityKey(page1Row0)}`;
    const k2 = `${segmentIndex}|${locatorIdentityKey(page2Row0)}`;
    expect(k1).not.toBe(k2);
  });

  test('encodes and decodes cursors', () => {
    const raw = {
      locatorReaderRank: 0,
      locatorSpineIndex: 2,
      locatorCharOffset: 128,
      locatorSpineHref: 'OEBPS/ch02.xhtml',
      locatorPage: -1,
      locatorLocation: '',
      segmentIndex: 4,
      locatorIdentityKey: 'epub:2:OEBPS/ch02.xhtml:128',
      segmentEntryId: 'entry-4',
    };
    const encoded = encodeManifestCursor(raw);
    expect(decodeManifestCursor(encoded)).toEqual(raw);
    expect(decodeManifestCursor('not-base64')).toBeNull();
    const malformed = Buffer.from(JSON.stringify({ bad: true }), 'utf8').toString('base64url');
    expect(decodeManifestCursor(malformed)).toBeNull();
  });

  test('clamps page size bounds', () => {
    expect(parseManifestPageSize(null)).toBe(150);
    expect(parseManifestPageSize('10')).toBe(25);
    expect(parseManifestPageSize('900')).toBe(500);
    expect(parseManifestPageSize('200')).toBe(200);
  });
});
