import { describe, expect, it } from 'vitest';
import { parse } from './parse';
import { compile } from './compile';
import { MAX_GROUP_DEPTH, type FieldType, type Group } from './types';

const FIELDS: Record<string, FieldType> = {
  genre: 'text', title: 'text', plot: 'fulltext', type: 'enum', minutes: 'number', release_date: 'date'
};

describe('parse', () => {
  it('parses a single quoted text atom as is', () => {
    expect(parse('genre:"Horror"', FIELDS)).toEqual({ match: 'all', children: [{ field: 'genre', operator: 'is', value: 'Horror' }] });
  });

  it('parses NOT as isNot / notMatches by field type', () => {
    expect(parse('NOT genre:"Horror"', FIELDS)).toEqual({ match: 'all', children: [{ field: 'genre', operator: 'isNot', value: 'Horror' }] });
    expect(parse('NOT plot:"car"', FIELDS)).toEqual({ match: 'all', children: [{ field: 'plot', operator: 'notMatches', value: 'car' }] });
  });

  it('parses a quoted fulltext atom as matches', () => {
    expect(parse('plot:"car"', FIELDS)).toEqual({ match: 'all', children: [{ field: 'plot', operator: 'matches', value: 'car' }] });
  });

  it('parses wildcard forms', () => {
    expect(parse('title:*night*', FIELDS)).toEqual({ match: 'all', children: [{ field: 'title', operator: 'contains', value: 'night' }] });
    expect(parse('title:The*', FIELDS)).toEqual({ match: 'all', children: [{ field: 'title', operator: 'startsWith', value: 'The' }] });
  });

  it('parses number/date ranges disambiguated by field type', () => {
    expect(parse('minutes:{30 TO *}', FIELDS)).toEqual({ match: 'all', children: [{ field: 'minutes', operator: 'gt', value: '30' }] });
    expect(parse('release_date:{* TO 2010-01-01}', FIELDS)).toEqual({ match: 'all', children: [{ field: 'release_date', operator: 'before', value: '2010-01-01' }] });
    expect(parse('minutes:[30 TO 90]', FIELDS)).toEqual({ match: 'all', children: [{ field: 'minutes', operator: 'between', value: '30', value2: '90' }] });
    expect(parse('minutes:42', FIELDS)).toEqual({ match: 'all', children: [{ field: 'minutes', operator: 'eq', value: '42' }] });
  });

  it('round-trips date ranges (yyyy-MM-dd <-> yyyyMMdd)', () => {
    for (const g of [
      { match: 'all' as const, children: [{ field: 'release_date', operator: 'after' as const, value: '2005-06-15' }] },
      { match: 'all' as const, children: [{ field: 'release_date', operator: 'between' as const, value: '2000-01-01', value2: '2010-12-31' }] }
    ]) {
      expect(parse(compile(g), FIELDS)).toEqual(g);
    }
  });

  it('parses a nested group', () => {
    expect(parse('type:"movie" AND (genre:"Horror" OR genre:"Thriller")', FIELDS)).toEqual({
      match: 'all',
      children: [
        { field: 'type', operator: 'is', value: 'movie' },
        { match: 'any', children: [
          { field: 'genre', operator: 'is', value: 'Horror' },
          { field: 'genre', operator: 'is', value: 'Thriller' }
        ] }
      ]
    });
  });

  it('parses three levels of nesting (#436)', () => {
    expect(parse('type:"movie" AND (genre:"Horror" OR (genre:"Thriller" AND (minutes:{90 TO *})))', FIELDS)).toEqual({
      match: 'all',
      children: [
        { field: 'type', operator: 'is', value: 'movie' },
        { match: 'any', children: [
          { field: 'genre', operator: 'is', value: 'Horror' },
          { match: 'all', children: [
            { field: 'genre', operator: 'is', value: 'Thriller' },
            { match: 'all', children: [{ field: 'minutes', operator: 'gt', value: '90' }] }
          ] }
        ] }
      ]
    });
  });

  it('round-trips a tree at the maximum nesting depth, and rejects one level deeper (#436)', () => {
    const leaf = (v: string) => ({ field: 'genre', operator: 'is' as const, value: v });
    const nest = (levels: number): Group => {
      let g: Group = { match: 'any', children: [leaf('deep-a'), leaf('deep-b')] };
      for (let i = 0; i < levels; i++) g = { match: 'all', children: [leaf(`lvl${i}`), g] };
      return g;
    };
    // `nest(n)` puts the innermost group at depth n, so MAX_GROUP_DEPTH is exactly at the cap.
    const atCap = nest(MAX_GROUP_DEPTH);
    expect(parse(compile(atCap), FIELDS)).toEqual(atCap);
    // One level deeper is out of subset — null, not a lossy best-effort tree.
    expect(parse(compile(nest(MAX_GROUP_DEPTH + 1)), FIELDS)).toBeNull();
  });

  it('returns null on out-of-subset input', () => {
    expect(parse('genre:"a" AND genre:"b" OR genre:"c"', FIELDS)).toBeNull(); // mixed AND/OR at one level
    expect(parse('unknown_field:"x"', FIELDS)).toBeNull();                     // unknown field
    expect(parse('title:jo~2', FIELDS)).toBeNull();                            // fuzzy — not in subset
    expect(parse('(genre:"a")x(genre:"b")', FIELDS)).toBeNull();               // adjacent groups, not one wrapped group
    expect(parse('(genre:"a"', FIELDS)).toBeNull();                            // unbalanced
  });
});

describe('parse: special-char round-trips', () => {
  it('round-trips quoted values with lucene specials (compile∘parse)', () => {
    for (const v of ['a*b', 'x y', 'a\\', 'has"quote', '(paren)', 'a AND b', 'a:b']) {
      const g = { match: 'all' as const, children: [{ field: 'genre', operator: 'is' as const, value: v }] };
      expect(parse(compile(g), FIELDS)).toEqual(g);
    }
  });
  it('round-trips text contains/startsWith values with specials', () => {
    for (const v of ['a*b', 'x y', 'plain']) {
      const c = { match: 'all' as const, children: [{ field: 'title', operator: 'contains' as const, value: v }] };
      expect(parse(compile(c), FIELDS)).toEqual(c);
    }
    for (const v of ['star*', 'x y', 'ab']) {
      const s = { match: 'all' as const, children: [{ field: 'title', operator: 'startsWith' as const, value: v }] };
      expect(parse(compile(s), FIELDS)).toEqual(s);
    }
  });
});

describe('parse — #435 relative dates', () => {
  const ft = { release_date: 'date', added_date: 'date' } as const;
  it('parses released_inthelast back to a relative rule', () => {
    expect(parse('released_inthelast:"7 day"', ft)).toEqual({
      match: 'all',
      children: [{ field: 'release_date', operator: 'inLast', value: '7', unit: 'day' }]
    });
  });
  it('parses added_notinthelast', () => {
    expect(parse('added_notinthelast:"3 week"', ft)).toEqual({
      match: 'all',
      children: [{ field: 'added_date', operator: 'notInLast', value: '3', unit: 'week' }]
    });
  });
});
