import { describe, expect, test } from 'vitest';
import { DISTINCT_CAP, profileData } from './profile';

describe('profileData', () => {
  test('profiles a JSON-array dataset fully', () => {
    const rows = [
      { city: 'Kyiv', pop: 2900000, capital: 'true' },
      { city: 'Lviv', pop: 720000, capital: 'false' },
    ];
    const profile = profileData(rows, 123);
    expect(profile.rowCount).toBe(2);
    expect(profile.columnCount).toBe(3);
    expect(profile.columns).toEqual(['city', 'pop', 'capital']);
    expect(profile.columnTypes).toEqual([
      { name: 'city', type: 'string' },
      { name: 'pop', type: 'number' },
      { name: 'capital', type: 'boolean' },
    ]);
    expect(profile.size).toBe(123);
  });

  test('null rows (URL) return the N/A profile but keep size', () => {
    const profile = profileData(null, 42);
    expect(profile.rowCount).toBeNull();
    expect(profile.columnCount).toBeNull();
    expect(profile.columns).toEqual([]);
    expect(profile.columnTypes).toEqual([]);
    expect(profile.size).toBe(42);
  });

  test('an empty array returns the N/A profile but keeps size', () => {
    const profile = profileData([], 7);
    expect(profile.rowCount).toBeNull();
    expect(profile.columnCount).toBeNull();
    expect(profile.columns).toEqual([]);
    expect(profile.size).toBe(7);
  });

  test('column order follows first-seen key order across ragged rows', () => {
    const rows = [{ a: 1 }, { b: 2, a: 3 }, { c: 4 }];
    const profile = profileData(rows, 0);
    expect(profile.columns).toEqual(['a', 'b', 'c']);
  });

  test('respects the sampling cap: rows beyond the head do not affect type', () => {
    // First 200 rows numeric; row 201 is text. With head-only sampling the
    // column still reads as number (an accepted, documented trade-off).
    const rows: Array<Record<string, unknown>> = [];
    for (let i = 0; i < 200; i++) rows.push({ v: i });
    rows.push({ v: 'tail-text' });
    const profile = profileData(rows, 0);
    expect(profile.rowCount).toBe(201); // count reflects the whole payload
    expect(profile.columnTypes).toEqual([{ name: 'v', type: 'number' }]);
  });
});

describe('profileData — column stats (cardinality + numeric extent)', () => {
  test('counts distinct values and reports numeric extent for number columns', () => {
    const rows = [
      { region: 'N', sales: 10 },
      { region: 'S', sales: -4 },
      { region: 'N', sales: 25 }, // region repeats → 2 distinct, not 3
    ];
    const stats = profileData(rows, 0).columnStats;
    expect(stats).toEqual([
      { name: 'region', distinct: 2, distinctCapped: false, numericExtent: null },
      { name: 'sales', distinct: 3, distinctCapped: false, numericExtent: { min: -4, max: 25 } },
    ]);
  });

  test('distinct counting is empty-insensitive and case/whitespace literal', () => {
    const rows = [{ c: 'A' }, { c: ' A ' }, { c: '' }, { c: null }, { c: 'b' }];
    // '  A  ' trims to 'A' (one key); '' and null are empties (no signal) → 2 distinct.
    expect(profileData(rows, 0).columnStats[0]).toEqual({
      name: 'c',
      distinct: 2,
      distinctCapped: false,
      numericExtent: null,
    });
  });

  test('caps distinct at DISTINCT_CAP and flags the overflow', () => {
    const rows = Array.from({ length: DISTINCT_CAP + 25 }, (_, i) => ({ id: `v${i}` }));
    const stat = profileData(rows, 0).columnStats[0];
    expect(stat.distinct).toBe(DISTINCT_CAP);
    expect(stat.distinctCapped).toBe(true);
  });

  test('numericExtent is null for a non-numeric column even if some cells parse', () => {
    // A 'string' column (one non-numeric value knocks the type down) carries no extent.
    const rows = [{ v: '1' }, { v: '2' }, { v: 'oops' }];
    const stat = profileData(rows, 0).columnStats[0];
    expect(stat.numericExtent).toBeNull();
  });

  test('N/A profile carries empty column stats', () => {
    expect(profileData(null, 9).columnStats).toEqual([]);
  });
});
