import { describe, expect, it } from 'vitest';
import {
  CURRENT_FONT_VERSION,
  createFontAsset,
  deserializeFontAsset,
  detectFontFormat,
  familyNameFromFileName,
  type FontAsset,
  type FontAxis,
  fontDataUri,
  fontFamilyStack,
  isVariableFont,
  parseFontAxes,
  primaryFamilyName,
  serializeFontAsset,
  variableFontDescriptors,
} from './font-asset';

const bytes = (n: number): ArrayBuffer => new Uint8Array(n).fill(1).buffer;

/**
 * Build a minimal SFNT with an `fvar` table carrying the given axes, so the
 * parser is exercised against real byte layout without committing a font binary.
 * Layout: 12-byte sfnt header, one 16-byte table record ('fvar'), then the fvar
 * table (16-byte header + 20 bytes per axis). Mirrors the offsets parseFontAxes
 * reads — verified against a real Fixel variable file during development.
 */
function buildVariableSfnt(axes: Array<[string, number, number, number]>): ArrayBuffer {
  const axisSize = 20;
  const fvarHeaderLen = 16;
  const fvarLen = fvarHeaderLen + axes.length * axisSize;
  const tableOffset = 12 + 16; // sfnt header + one table record
  const buf = new ArrayBuffer(tableOffset + fvarLen);
  const v = new DataView(buf);
  v.setUint32(0, 0x00010000); // sfntVersion (TrueType)
  v.setUint16(4, 1); // numTables
  for (let i = 0; i < 4; i++) v.setUint8(12 + i, 'fvar'.charCodeAt(i));
  v.setUint32(12 + 8, tableOffset); // table record: offset
  v.setUint32(12 + 12, fvarLen); // table record: length
  v.setUint16(tableOffset, 1); // fvar major version
  v.setUint16(tableOffset + 4, fvarHeaderLen); // axesArrayOffset
  v.setUint16(tableOffset + 8, axes.length); // axisCount
  v.setUint16(tableOffset + 10, axisSize); // axisSize
  const base = tableOffset + fvarHeaderLen;
  axes.forEach(([tag, min, def, max], i) => {
    const a = base + i * axisSize;
    for (let j = 0; j < 4; j++) v.setUint8(a + j, tag.charCodeAt(j));
    v.setInt32(a + 4, min * 65536);
    v.setInt32(a + 8, def * 65536);
    v.setInt32(a + 12, max * 65536);
  });
  return buf;
}

describe('detectFontFormat', () => {
  it('maps supported extensions, case-insensitively', () => {
    expect(detectFontFormat('Inter.woff2')).toBe('woff2');
    expect(detectFontFormat('Inter.WOFF')).toBe('woff');
    expect(detectFontFormat('Inter.ttf')).toBe('ttf');
    expect(detectFontFormat('Inter.OTF')).toBe('otf');
  });

  it('reads the last extension and rejects unsupported / extensionless names', () => {
    expect(detectFontFormat('My.Font.woff2')).toBe('woff2');
    expect(detectFontFormat('Inter.eot')).toBeNull();
    expect(detectFontFormat('Inter')).toBeNull();
    expect(detectFontFormat('')).toBeNull();
  });
});

describe('familyNameFromFileName', () => {
  it('drops the extension and normalizes separators', () => {
    expect(familyNameFromFileName('Inter-Regular.ttf')).toBe('Inter Regular');
    expect(familyNameFromFileName('my_cool_font.woff2')).toBe('my cool font');
    expect(familyNameFromFileName('Spaced  Out.otf')).toBe('Spaced Out');
  });

  it('falls back when the name reduces to nothing', () => {
    expect(familyNameFromFileName('.woff2')).toBe('Custom font');
  });
});

describe('createFontAsset', () => {
  it('stamps version/timestamps/size and defaults source to file', () => {
    const now = new Date('2026-06-15T12:00:00.000Z');
    const asset = createFontAsset({
      family: 'My Font',
      data: bytes(2048),
      format: 'woff2',
      fileName: 'My-Font.woff2',
      now,
      id: 7,
    });
    expect(asset).toMatchObject({
      id: 7,
      version: CURRENT_FONT_VERSION,
      family: 'My Font',
      format: 'woff2',
      fileName: 'My-Font.woff2',
      source: 'file',
      size: 2048,
      created: now.toISOString(),
      modified: now.toISOString(),
    });
  });

  it('carries an explicit source', () => {
    expect(
      createFontAsset({
        family: 'G',
        data: bytes(1),
        format: 'woff2',
        fileName: 'g.woff2',
        source: 'google',
      }).source,
    ).toBe('google');
  });
});

describe('fontFamilyStack', () => {
  it('quotes the family and appends a generic fallback', () => {
    expect(fontFamilyStack('My Font')).toBe('"My Font", sans-serif');
  });
});

describe('parseFontAxes', () => {
  it('reads variation axes from an SFNT fvar table (ttf/otf)', () => {
    const sfnt = buildVariableSfnt([
      ['wght', 100, 400, 900],
      ['wdth', 75, 100, 100],
    ]);
    expect(parseFontAxes(sfnt, 'ttf')).toEqual([
      { tag: 'wght', min: 100, default: 400, max: 900 },
      { tag: 'wdth', min: 75, default: 100, max: 100 },
    ]);
    expect(parseFontAxes(sfnt, 'otf')).toHaveLength(2);
  });

  it('returns [] for a static font (no fvar table)', () => {
    // A valid sfnt header claiming zero tables — no fvar to find.
    const buf = new ArrayBuffer(12);
    new DataView(buf).setUint32(0, 0x00010000);
    expect(parseFontAxes(buf, 'ttf')).toEqual([]);
  });

  it('treats woff/woff2 as static — their tables are compressed, not parsed', () => {
    const sfnt = buildVariableSfnt([['wght', 100, 400, 900]]);
    expect(parseFontAxes(sfnt, 'woff2')).toEqual([]);
    expect(parseFontAxes(sfnt, 'woff')).toEqual([]);
  });

  it('degrades to [] on a malformed file rather than throwing', () => {
    expect(parseFontAxes(new Uint8Array([1, 2, 3]).buffer, 'ttf')).toEqual([]);
  });
});

describe('variable-font helpers', () => {
  const wght: FontAxis = { tag: 'wght', min: 100, default: 400, max: 900 };
  const wdth: FontAxis = { tag: 'wdth', min: 75, default: 100, max: 100 };

  it('isVariableFont reflects whether axes are present', () => {
    expect(isVariableFont([wght])).toBe(true);
    expect(isVariableFont([])).toBe(false);
    expect(isVariableFont(undefined)).toBe(false);
  });

  it('maps wght→weight range and wdth→stretch range; ignores other axes', () => {
    expect(variableFontDescriptors([wght, wdth])).toEqual({
      weight: '100 900',
      stretch: '75% 100%',
    });
    expect(variableFontDescriptors([{ tag: 'opsz', min: 8, default: 14, max: 144 }])).toEqual({});
    expect(variableFontDescriptors(undefined)).toEqual({});
  });
});

describe('primaryFamilyName', () => {
  it('extracts the first comma-segment and strips quotes', () => {
    expect(primaryFamilyName('"My Brand", sans-serif')).toBe('My Brand');
    expect(primaryFamilyName("'Inter', system-ui, sans-serif")).toBe('Inter');
    expect(primaryFamilyName('system-ui, -apple-system')).toBe('system-ui');
    expect(primaryFamilyName('Brand')).toBe('Brand');
  });
});

describe('fontDataUri', () => {
  it('builds a base64 data URL with the format MIME type', () => {
    const asset = createFontAsset({
      family: 'Brand',
      data: new Uint8Array([1, 2, 3, 4]).buffer,
      format: 'woff2',
      fileName: 'brand.woff2',
    });
    expect(fontDataUri(asset)).toBe(`data:font/woff2;base64,${btoa('\x01\x02\x03\x04')}`);
  });
});

describe('serializeFontAsset / deserializeFontAsset', () => {
  const NOW = new Date('2026-06-16T08:00:00.000Z');

  function sample(overrides: Partial<FontAsset> = {}): FontAsset {
    return {
      ...createFontAsset({
        family: 'Brand',
        data: new Uint8Array([10, 20, 30, 40, 50]).buffer,
        format: 'woff2',
        fileName: 'brand.woff2',
        now: NOW,
        id: 7,
      }),
      ...overrides,
    };
  }

  it('round-trips a font through base64 without losing its bytes or metadata', () => {
    const original = sample({ modified: '2026-06-16T09:00:00.000Z' });
    const restored = deserializeFontAsset(serializeFontAsset(original));
    expect(restored).not.toBeNull();
    expect(new Uint8Array(restored!.data)).toEqual(new Uint8Array(original.data));
    expect(restored!.family).toBe('Brand');
    expect(restored!.format).toBe('woff2');
    expect(restored!.fileName).toBe('brand.woff2');
    expect(restored!.size).toBe(5);
    expect(restored!.created).toBe(NOW.toISOString());
    expect(restored!.modified).toBe('2026-06-16T09:00:00.000Z');
  });

  it('preserves variable-font axes through the round-trip', () => {
    const axes: FontAxis[] = [{ tag: 'wght', min: 100, default: 400, max: 900 }];
    const restored = deserializeFontAsset(serializeFontAsset(sample({ axes })));
    expect(restored!.axes).toEqual(axes);
  });

  it('re-derives size from the decoded bytes (not a stale stored value)', () => {
    const serialized = { ...serializeFontAsset(sample()), size: 999 };
    expect(deserializeFontAsset(serialized)!.size).toBe(5);
  });

  it('returns null for a record missing family or data, or with bad base64', () => {
    expect(deserializeFontAsset(null)).toBeNull();
    expect(deserializeFontAsset({ data: 'AAEC' })).toBeNull();
    expect(deserializeFontAsset({ family: 'X' })).toBeNull();
    expect(deserializeFontAsset({ family: 'X', data: '' })).toBeNull();
    expect(deserializeFontAsset({ family: 'X', data: '!!not-base64!!' })).toBeNull();
  });

  it('falls back to a derived format when the stored one is absent/unknown', () => {
    const serialized = { ...serializeFontAsset(sample()), format: 'bogus' };
    expect(deserializeFontAsset(serialized)!.format).toBe('woff2'); // from fileName
  });

  it('stores timestamps verbatim and never throws on a corrupt created (would abort import)', () => {
    const serialized = { ...serializeFontAsset(sample()), created: 'not-a-date', modified: '' };
    const restored = deserializeFontAsset(serialized);
    expect(restored).not.toBeNull();
    expect(restored!.created).toBe('not-a-date');
  });
});
