import { describe, it, expect } from 'vitest';
import { format } from './format';

function formatted(src: string): string {
	const r = format(src);
	if (!r.ok) throw new Error(`expected format to succeed, got: ${r.error.message}`);
	return r.text;
}

describe('format — basic shape', () => {
	it('normalizes a simple let with operator spacing', () => {
		const out = formatted('let   x=1+2 *  3;');
		expect(out).toBe('let x = 1 + 2 * 3;\n');
	});

	it('renders a fn declaration with body', () => {
		const out = formatted('fn  process(order,user){order.total}');
		expect(out).toBe(
			'fn process(order, user) {\n' +
				'\torder.total\n' +
				'}\n'
		);
	});

	it('does not insert a blank between fn decls the user did not separate', () => {
		// Strict preserve-only policy: no source blank => no emitted blank.
		const out = formatted('fn a(){1}fn b(){2}');
		expect(out).toBe('fn a() {\n\t1\n}\nfn b() {\n\t2\n}\n');
	});

	it('renders if / else if / else with blocks', () => {
		const out = formatted('if a{1}else if b{2}else{3}');
		expect(out).toBe(
			'if a {\n\t1\n} else if b {\n\t2\n} else {\n\t3\n}\n'
		);
	});

	it('renders an object-map literal inline when short', () => {
		const out = formatted('let o=#{a:1,b:2};');
		expect(out).toBe('let o = #{ a: 1, b: 2 };\n');
	});

	it('renders log::info as a namespace call', () => {
		const out = formatted('log::info(  "hi"  );');
		expect(out).toBe('log::info("hi");\n');
	});

	it('preserves comments verbatim before statements', () => {
		const out = formatted('// docstring\nfn process(){1}');
		expect(out).toBe(
			'// docstring\nfn process() {\n\t1\n}\n'
		);
	});

	it('keeps block comments verbatim', () => {
		const out = formatted('/* keep me */ let x = 1;');
		expect(out).toContain('/* keep me */');
		expect(out).toContain('let x = 1;');
	});

	it('emits an empty block as `{}` without padding', () => {
		const out = formatted('fn noop(){}');
		expect(out).toBe('fn noop() {}\n');
	});

	it('preserves string literals verbatim', () => {
		const out = formatted('let s = "hello\\nworld";');
		expect(out).toBe('let s = "hello\\nworld";\n');
	});
});

describe('format — reflow', () => {
	it('reflows a long argument list onto separate lines', () => {
		const src =
			'process(aaaaaaaaaa, bbbbbbbbbb, cccccccccc, dddddddddd, eeeeeeeeee, ffffffffff, gggggggggg, hhhhhhhhhh);';
		const out = formatted(src);
		// Should contain at least one newline inside the parens (multi-line).
		const callBlock = out.slice(out.indexOf('('), out.lastIndexOf(')') + 1);
		expect(callBlock).toContain('\n');
		expect(callBlock.endsWith(',\n)')).toBe(true);
	});

	it('keeps short argument lists inline', () => {
		const out = formatted('process(1, 2, 3);');
		expect(out).toBe('process(1, 2, 3);\n');
	});
});

describe('format — blank-line preservation', () => {
	it('preserves a single blank line between statements', () => {
		const src = 'let a = 1;\n\nlet b = 2;';
		expect(formatted(src)).toBe('let a = 1;\n\nlet b = 2;\n');
	});

	it('collapses multiple blank lines to a single one', () => {
		const src = 'let a = 1;\n\n\n\nlet b = 2;';
		expect(formatted(src)).toBe('let a = 1;\n\nlet b = 2;\n');
	});

	it('preserves blanks inside block bodies', () => {
		const src = 'fn process() {\n\tlet a = 1;\n\n\tlet b = 2;\n}';
		expect(formatted(src)).toBe('fn process() {\n\tlet a = 1;\n\n\tlet b = 2;\n}\n');
	});

	it('does not invent blanks between adjacent statements', () => {
		expect(formatted('let a=1;let b=2;')).toBe('let a = 1;\nlet b = 2;\n');
	});
});

describe('format — parse failures', () => {
	it('returns ok=false with a Rhai-flavored message and 1-based line/column', () => {
		// Pattern from the user complaint: `let;` should surface as
		// "Expecting name of a variable" at line/column.
		const r = format('let msg = ctx.request.params.name;\nlet;\n');
		expect(r.ok).toBe(false);
		if (!r.ok) {
			expect(r.error.message).toBe('Expecting name of a variable');
			expect(r.error.line).toBe(2);
			expect(r.error.column).toBe(4);
			expect(r.error.offset).toBeGreaterThanOrEqual(0);
		}
	});

	it('reports script-incomplete on truncated input', () => {
		// `fn` alone — the parser expects a function name and hits EOF.
		const r = format('fn');
		expect(r.ok).toBe(false);
		if (!r.ok) expect(r.error.message).toMatch(/script is incomplete/i);
	});

	it('does not partially rewrite when parsing fails', () => {
		const r = format('let x = 1; this is garbage');
		expect(r.ok).toBe(false);
	});
});

describe('format — idempotent', () => {
	it('formatting twice yields the same output', () => {
		const src = `
			fn process(order,user) {
				if order.total > 100 {
					log::info("big", #{id:order.id});
				} else {
					log::info("small");
				}
				return order;
			}
		`;
		const a = formatted(src);
		const b = formatted(a);
		expect(b).toBe(a);
	});
});
