import { describe, it, expect } from 'vitest';
import { parse } from './parser';
import type { BinaryExpr, ExprStmt, FnDecl, LetStmt } from './ast';

describe('parser — declarations', () => {
	it('parses a let binding with initializer', () => {
		const { program, errors } = parse('let x = 1 + 2;');
		expect(errors).toEqual([]);
		expect(program.stmts).toHaveLength(1);
		const let_ = program.stmts[0] as LetStmt;
		expect(let_.kind).toBe('Let');
		expect(let_.name).toBe('x');
		expect(let_.init?.kind).toBe('Binary');
	});

	it('parses a const binding', () => {
		const { program, errors } = parse('const PI = 3.14;');
		expect(errors).toEqual([]);
		expect(program.stmts[0]).toMatchObject({ kind: 'Const', name: 'PI' });
	});

	it('parses fn declarations with parameters', () => {
		const { program, errors } = parse('fn process(order, user) { order.total }');
		expect(errors).toEqual([]);
		const fn = program.stmts[0] as FnDecl;
		expect(fn.kind).toBe('FnDecl');
		expect(fn.name).toBe('process');
		expect(fn.params.map((p) => p.name)).toEqual(['order', 'user']);
		expect(fn.body.stmts).toHaveLength(1);
	});
});

describe('parser — expressions', () => {
	it('respects binary precedence (* before +)', () => {
		const { program } = parse('let a = 1 + 2 * 3;');
		const e = (program.stmts[0] as LetStmt).init as BinaryExpr;
		expect(e.kind).toBe('Binary');
		expect(e.op).toBe('+');
		const right = e.right as BinaryExpr;
		expect(right.op).toBe('*');
	});

	it('parses method chains (member + call + index)', () => {
		const { program, errors } = parse('let x = ctx.request.body["k"];');
		expect(errors).toEqual([]);
		const init = (program.stmts[0] as LetStmt).init!;
		expect(init.kind).toBe('Index');
	});

	it('parses log::info("hi") as Call(Member(Ident(log), "info"), ["hi"])', () => {
		const { program, errors } = parse('log::info("hi");');
		expect(errors).toEqual([]);
		const stmt = program.stmts[0] as ExprStmt;
		expect(stmt.expr.kind).toBe('Call');
	});

	it('parses object-map literal #{} with keys', () => {
		const { program, errors } = parse('let o = #{ a: 1, b: 2 };');
		expect(errors).toEqual([]);
		const init = (program.stmts[0] as LetStmt).init!;
		expect(init.kind).toBe('ObjectMap');
		if (init.kind === 'ObjectMap') {
			expect(init.entries.map((e) => e.key)).toEqual(['a', 'b']);
		}
	});

	it('parses array literals', () => {
		const { program, errors } = parse('let xs = [1, 2, 3];');
		expect(errors).toEqual([]);
		expect((program.stmts[0] as LetStmt).init!.kind).toBe('Array');
	});

	it('parses if-as-expression for let RHS', () => {
		const { program, errors } = parse('let x = if true { 1 } else { 2 };');
		expect(errors).toEqual([]);
		expect((program.stmts[0] as LetStmt).init!.kind).toBe('IfExpr');
	});
});

describe('parser — control flow', () => {
	it('parses while, for, loop', () => {
		const { errors: e1 } = parse('while true { break; }');
		const { errors: e2 } = parse('for x in [1, 2] { x }');
		const { errors: e3 } = parse('loop { break; }');
		expect(e1).toEqual([]);
		expect(e2).toEqual([]);
		expect(e3).toEqual([]);
	});

	it('parses if / else if / else chains', () => {
		const { program, errors } = parse(`
			if a { 1 } else if b { 2 } else { 3 }
		`);
		expect(errors).toEqual([]);
		const stmt = program.stmts[0] as ExprStmt;
		expect(stmt.expr.kind).toBe('IfExpr');
	});

	it('parses try / catch with binding', () => {
		const { errors } = parse('try { foo(); } catch (e) { log::error(e); }');
		expect(errors).toEqual([]);
	});
});

describe('parser — error tolerance', () => {
	it('is lenient about missing semicolons between statements', () => {
		// The parser accepts implicit statement separation so completions
		// remain useful while the user is still typing. Both bindings
		// should land in the program regardless of the missing `;`.
		const { program } = parse('let x = 1 let y = 2;');
		const names = program.stmts.flatMap((s) => (s.kind === 'Let' ? [s.name] : []));
		expect(names).toContain('x');
		expect(names).toContain('y');
	});

	it('does not loop forever on garbage', () => {
		const { errors } = parse('@@@ ### }}}');
		expect(errors.length).toBeGreaterThan(0);
	});

	it('recovers after a bad statement and parses the next one', () => {
		const { program } = parse('let = ; let y = 2;');
		const y = program.stmts.find((s) => s.kind === 'Let' && s.name === 'y');
		expect(y).toBeDefined();
	});
});
