import { describe, expect, it } from 'vitest';
import { compile } from './compile';
import { parse } from './parse';
import { normalizeGroup } from './validation';
import { isGroup, MAX_GROUP_DEPTH, type DateUnit, type FieldType, type Group, type Operator, type Rule } from './types';

const FIELDS: Record<string, FieldType> = {
  genre: 'text', title: 'text', plot: 'fulltext', type: 'enum', minutes: 'number',
  release_date: 'date', added_date: 'date'
};
const BY_TYPE: Record<FieldType, string[]> = {
  text: ['genre', 'title'], fulltext: ['plot'], enum: ['type'], number: ['minutes'],
  date: ['release_date', 'added_date']
};
const OPS: Record<FieldType, Operator[]> = {
  text: ['is', 'isNot', 'contains', 'startsWith'],
  fulltext: ['matches', 'notMatches'],
  enum: ['is', 'isNot'],
  number: ['eq', 'gt', 'lt', 'between'],
  date: ['before', 'after', 'between']
};

// Deterministic LCG so a failing case is reproducible.
function lcg(seed: number) {
  let s = seed >>> 0;
  return () => {
    s = (1664525 * s + 1013904223) >>> 0;
    return s / 0xffffffff;
  };
}
const pick = <T,>(rng: () => number, arr: T[]): T => arr[Math.floor(rng() * arr.length)];
// Include Lucene specials, spaces, quotes and backslashes: escaping must be TOTAL, so string
// values must round-trip regardless of content (this is what catches escaping bugs the pure
// alphanumeric generator would miss). Numbers/dates are generated separately in makeRule.
const VALUE_CHARS = 'abcdefghijABCDEFGHIJ0123456789 *?"\\():-';
const token = (rng: () => number): string => {
  const n = 3 + Math.floor(rng() * 5);
  let out = '';
  for (let i = 0; i < n; i++) out += VALUE_CHARS[Math.floor(rng() * VALUE_CHARS.length)];
  return out;
};

function makeRule(rng: () => number): Rule {
  const type = pick(rng, Object.keys(BY_TYPE) as FieldType[]);
  const field = pick(rng, BY_TYPE[type]);

  // #435: sometimes emit a relative-date rule for date fields, alongside before/after/between.
  if (type === 'date' && rng() < 0.5) {
    const operator = rng() < 0.5 ? 'inLast' : 'notInLast';
    const unit = pick(rng, ['day', 'week', 'month', 'year'] as DateUnit[]);
    return { field, operator, value: String(1 + Math.floor(rng() * 30)), unit };
  }

  const operator = pick(rng, OPS[type]);
  if (operator === 'between') {
    return type === 'date'
      ? { field, operator, value: '2000-01-01', value2: '2010-01-01' }
      : { field, operator, value: '10', value2: '90' };
  }
  if (type === 'number') return { field, operator, value: String(1 + Math.floor(rng() * 500)) };
  if (type === 'date') return { field, operator, value: '2005-06-15' };
  return { field, operator, value: token(rng) };
}

// #436: groups nest to MAX_GROUP_DEPTH levels below the root (which is depth 0), so the generator
// recurses on the same shared cap the UI and parser use rather than a hardcoded one level.
function makeGroup(rng: () => number, depth: number): Group {
  const match = rng() < 0.5 ? 'all' : 'any';
  const count = 2 + Math.floor(rng() * 3); // >= 2 children
  const children: Array<Rule | Group> = [];
  for (let i = 0; i < count; i++) {
    if (depth < MAX_GROUP_DEPTH && rng() < 0.35) children.push(makeGroup(rng, depth + 1));
    else children.push(makeRule(rng));
  }
  return { match, children };
}

function maxDepth(group: Group, depth = 0): number {
  return group.children.reduce(
    (acc, c) => (isGroup(c) ? Math.max(acc, maxDepth(c, depth + 1)) : acc),
    depth
  );
}

describe('round-trip: parse(compile(tree)) === tree', () => {
  it('holds over 500 generated trees', () => {
    const rng = lcg(12345);
    let deepest = 0;
    for (let i = 0; i < 500; i++) {
      const tree = makeGroup(rng, 0);
      deepest = Math.max(deepest, maxDepth(tree));
      const text = compile(tree);
      const back = parse(text, FIELDS);
      expect(back, `seed-iter ${i} failed for: ${text}`).toEqual(normalizeGroup(tree));
    }
    // Completeness guard: a generator that silently stopped nesting would still pass every
    // assertion above, so pin that the corpus actually reached the full cap.
    expect(deepest).toBe(MAX_GROUP_DEPTH);
  });
});
