import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { parse } from '../src/parser.js';
import { transpile } from '../src/transpiler.js';

/**
 * Helper: transpile a Push3 source string, returning the result.
 * The transpiler primes 8 input slots on the DYADIC stack, so the program
 * operates on top of those.
 */
function run(src: string) {
  const ast = parse(src);
  return transpile(ast);
}

// ---------------------------------------------------------------------------
// Stack underflow detection
// ---------------------------------------------------------------------------

describe('stack underflow', () => {
  it('DYADIC.+ underflows when fewer than 2 extra values beyond inputs', () => {
    // After 8 input slots are primed, DYADIC.+ pops two. Two pops from 8 is
    // fine (no underflow). But after popping all 8 inputs + 1 more, underflow.
    // Program: pop all 8 inputs, then try DYADIC.+ on an empty stack.
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.+
    )`;
    assert.throws(() => run(src), /DYADIC stack underflow/);
  });

  it('DYADIC.SWAP underflows on empty stack', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.SWAP
    )`;
    assert.throws(() => run(src), /DYADIC stack underflow/);
  });

  it('DYADIC.DUP underflows on empty stack', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.DUP
    )`;
    assert.throws(() => run(src), /DYADIC stack underflow/);
  });

  it('DYADIC.POP underflows on empty stack', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP
    )`;
    assert.throws(() => run(src), /DYADIC stack underflow/);
  });

  it('BOOLEAN.NOT underflows on empty boolean stack', () => {
    const src = '(BOOLEAN.NOT)';
    assert.throws(() => run(src), /BOOLEAN stack underflow/);
  });

  it('BOOLEAN.AND underflows on empty boolean stack', () => {
    const src = '(BOOLEAN.AND)';
    assert.throws(() => run(src), /BOOLEAN stack underflow/);
  });

  it('BOOLEAN.OR underflows with only one boolean', () => {
    const src = '(TRUE BOOLEAN.OR)';
    assert.throws(() => run(src), /BOOLEAN stack underflow/);
  });

  it('DYADIC.DEFINE underflows NAME stack', () => {
    // Push a value on DYADIC but no name on NAME stack
    const src = '(42 DYADIC.DEFINE)';
    assert.throws(() => run(src), /NAME stack underflow/);
  });

  it('DYADIC.> underflows when only one value on stack after clearing', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.>
    )`;
    assert.throws(() => run(src), /DYADIC stack underflow/);
  });

  it('EXEC.IF underflows when boolean stack is empty', () => {
    // EXEC.IF needs a boolean condition — pops from bStack
    const src = '(EXEC.IF (1) (2))';
    assert.throws(() => run(src), /BOOLEAN stack underflow/);
  });
});

// ---------------------------------------------------------------------------
// Stack overflow / excess values — transpiler handles gracefully
// ---------------------------------------------------------------------------

describe('stack overflow / excess values', () => {
  it('extra values on DYADIC stack are silently discarded (top 4 taken)', () => {
    // Push 6 values on top of the 8 inputs = 14 total. Top 4 become outputs.
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      100 200 300 400 500 600
    )`;
    const result = run(src);
    // Top of stack = 600 → ci, 500 → anchorShare, 400 → anchorWidth, 300 → discoveryDepth
    assert.equal(result.ciVar, '600');
    assert.equal(result.anchorShareVar, '500');
    assert.equal(result.anchorWidthVar, '400');
    assert.equal(result.discoveryDepthVar, '300');
  });

  it('fewer than 4 values uses bear defaults for missing positions', () => {
    // Clear all 8, push only 1 value
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      42
    )`;
    const result = run(src);
    assert.equal(result.ciVar, '42');
    // Missing positions get bear defaults
    assert.equal(result.anchorShareVar, '300000000000000000');
    assert.equal(result.anchorWidthVar, '100');
    assert.equal(result.discoveryDepthVar, '300000000000000000');
  });

  it('empty program (no instructions) uses inputs as outputs', () => {
    // No instructions at all — the 8 primed input slots remain.
    // Top 4 = inputs[0] through inputs[3].
    const src = '()';
    const result = run(src);
    assert.equal(result.ciVar, 'uint256(inputs[0].mantissa)');
    assert.equal(result.anchorShareVar, 'uint256(inputs[1].mantissa)');
    assert.equal(result.anchorWidthVar, 'uint256(inputs[2].mantissa)');
    assert.equal(result.discoveryDepthVar, 'uint256(inputs[3].mantissa)');
  });
});

// ---------------------------------------------------------------------------
// EXEC.IF branching — balanced branches
// ---------------------------------------------------------------------------

describe('EXEC.IF balanced branches', () => {
  it('emits if/else for simple balanced branches', () => {
    // Clear stack, push condition value, compare, then branch
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      100 50 DYADIC.>
      EXEC.IF
      (10 20 30 40)
      (1 2 3 4)
    )`;
    const result = run(src);
    // Both branches push 4 values; the result depends on the condition
    // The output should contain if/else structure
    const body = result.functionBody.join('\n');
    assert.ok(body.includes('if ('), 'should contain if statement');
    // Result vars should be used for differing positions
    assert.ok(result.ciVar !== undefined);
    assert.ok(result.anchorShareVar !== undefined);
  });

  it('both branches produce same value → no result variable needed', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      100 50 DYADIC.>
      EXEC.IF
      (10 20 30 99)
      (1 2 3 99)
    )`;
    const result = run(src);
    // Both branches have 99 at bottom (same position from before) — doesn't need result var for that
    // But top 3 differ → need result vars for those
    const body = result.functionBody.join('\n');
    assert.ok(body.includes('if ('), 'should emit if/else');
  });
});

// ---------------------------------------------------------------------------
// EXEC.IF branching — unbalanced branches
// ---------------------------------------------------------------------------

describe('EXEC.IF unbalanced branches', () => {
  it('true branch pushes more DYADIC values than false branch → error', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      100 50 DYADIC.>
      EXEC.IF
      (10 20)
      (10)
    )`;
    assert.throws(() => run(src), /DYADIC stack depth mismatch/);
  });

  it('false branch pushes more DYADIC values than true branch → error', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      100 50 DYADIC.>
      EXEC.IF
      (10)
      (10 20)
    )`;
    assert.throws(() => run(src), /DYADIC stack depth mismatch/);
  });

  it('error message includes both branch depths', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      100 50 DYADIC.>
      EXEC.IF
      (10 20 30)
      (10)
    )`;
    assert.throws(() => run(src), /true branch produces 3.*false branch produces 1/);
  });

  it('one branch pushes extra, the other is no-op → depth mismatch error', () => {
    // True branch pushes 2 extra values, false branch leaves stack unchanged
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      50
      100 50 DYADIC.>
      EXEC.IF
      (99 88)
      ()
    )`;
    assert.throws(() => run(src), /DYADIC stack depth mismatch/);
  });

  it('EXEC.IF with missing branches throws', () => {
    // EXEC.IF needs exactly 2 subsequent items
    const src = `(
      TRUE EXEC.IF (1)
    )`;
    assert.throws(() => run(src), /EXEC\.IF: missing branches/);
  });

  it('BOOLEAN stack depth mismatch between branches → error', () => {
    // true branch pushes a boolean comparison, false branch does not
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      10 20 30 40
      TRUE
      EXEC.IF
      (10 5 DYADIC.>)
      ()
    )`;
    assert.throws(() => run(src), /BOOLEAN stack depth mismatch/);
  });
});

// ---------------------------------------------------------------------------
// EXEC.IF — nested branching
// ---------------------------------------------------------------------------

describe('EXEC.IF nested branches', () => {
  it('nested EXEC.IF produces valid output', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      100 50 DYADIC.>
      EXEC.IF
      (
        100 80 DYADIC.>
        EXEC.IF
        (1 2 3 4)
        (5 6 7 8)
      )
      (10 20 30 40)
    )`;
    const result = run(src);
    const body = result.functionBody.join('\n');
    // Should contain nested if blocks
    const ifCount = (body.match(/if \(/g) || []).length;
    assert.ok(ifCount >= 2, `expected at least 2 if statements, got ${ifCount}`);
  });
});

// ---------------------------------------------------------------------------
// Arithmetic and comparisons
// ---------------------------------------------------------------------------

describe('arithmetic operations', () => {
  it('DYADIC.+ produces addition expression', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      10 20 DYADIC.+
    )`;
    const result = run(src);
    assert.equal(result.ciVar, '(10 + 20)');
  });

  it('DYADIC.- produces subtraction expression', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      10 3 DYADIC.-
    )`;
    const result = run(src);
    assert.equal(result.ciVar, '(10 - 3)');
  });

  it('DYADIC.* produces multiplication expression', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      5 6 DYADIC.*
    )`;
    const result = run(src);
    assert.equal(result.ciVar, '(5 * 6)');
  });

  it('DYADIC./ produces safe division expression', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      10 3 DYADIC./
    )`;
    const result = run(src);
    assert.equal(result.ciVar, '(3 == 0 ? 0 : 10 / 3)');
  });

  it('DYADIC.> produces comparison on boolean stack', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      10 5 DYADIC.>
      EXEC.IF (1 2 3 4) (5 6 7 8)
    )`;
    const result = run(src);
    const body = result.functionBody.join('\n');
    assert.ok(body.includes('(10 > 5)'), 'condition should contain (10 > 5)');
  });

  it('DYADIC.< produces less-than comparison', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      3 9 DYADIC.<
      EXEC.IF (1 2 3 4) (5 6 7 8)
    )`;
    const result = run(src);
    const body = result.functionBody.join('\n');
    assert.ok(body.includes('(3 < 9)'));
  });

  it('DYADIC.>= produces >= comparison', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      3 3 DYADIC.>=
      EXEC.IF (1 2 3 4) (5 6 7 8)
    )`;
    const result = run(src);
    const body = result.functionBody.join('\n');
    assert.ok(body.includes('(3 >= 3)'));
  });

  it('DYADIC.<= produces <= comparison', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      5 10 DYADIC.<=
      EXEC.IF (1 2 3 4) (5 6 7 8)
    )`;
    const result = run(src);
    const body = result.functionBody.join('\n');
    assert.ok(body.includes('(5 <= 10)'));
  });
});

// ---------------------------------------------------------------------------
// Boolean operations
// ---------------------------------------------------------------------------

describe('boolean operations', () => {
  it('BOOLEAN.NOT negates a boolean', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      TRUE BOOLEAN.NOT
      EXEC.IF (1 2 3 4) (5 6 7 8)
    )`;
    const result = run(src);
    const body = result.functionBody.join('\n');
    assert.ok(body.includes('!(true)'));
  });

  it('BOOLEAN.AND combines two booleans', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      TRUE FALSE BOOLEAN.AND
      EXEC.IF (1 2 3 4) (5 6 7 8)
    )`;
    const result = run(src);
    const body = result.functionBody.join('\n');
    assert.ok(body.includes('(true && false)'));
  });

  it('BOOLEAN.OR combines two booleans', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      TRUE FALSE BOOLEAN.OR
      EXEC.IF (1 2 3 4) (5 6 7 8)
    )`;
    const result = run(src);
    const body = result.functionBody.join('\n');
    assert.ok(body.includes('(true || false)'));
  });
});

// ---------------------------------------------------------------------------
// Name binding and DEFINE
// ---------------------------------------------------------------------------

describe('DYADIC.DEFINE and name binding', () => {
  it('binds a name and retrieves it', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      42 FOO DYADIC.DEFINE
      FOO
    )`;
    const result = run(src);
    // FOO should be bound to 'foo' variable, and ci should reference it
    assert.equal(result.ciVar, 'foo');
    const body = result.functionBody.join('\n');
    assert.ok(body.includes('uint256 foo = uint256(42);'));
  });

  it('bound names push to dStack, not nameStack', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      99 X DYADIC.DEFINE
      X X DYADIC.+
    )`;
    const result = run(src);
    assert.equal(result.ciVar, '(x + x)');
  });
});

// ---------------------------------------------------------------------------
// DYADIC.DUP and DYADIC.SWAP
// ---------------------------------------------------------------------------

describe('DYADIC.DUP and DYADIC.SWAP', () => {
  it('DYADIC.DUP duplicates top of stack', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      42 DYADIC.DUP DYADIC.+
    )`;
    const result = run(src);
    // DUP materialises into dup0, then + is (dup0 + dup0)
    assert.ok(result.ciVar.includes('dup0'));
    const body = result.functionBody.join('\n');
    assert.ok(body.includes('uint256 dup0 = uint256(42);'));
  });

  it('DYADIC.SWAP exchanges top two values', () => {
    const src = `(
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      10 20 DYADIC.SWAP
    )`;
    const result = run(src);
    // After SWAP: bottom=20, top=10
    assert.equal(result.ciVar, '10');
    assert.equal(result.anchorShareVar, '20');
  });
});

// ---------------------------------------------------------------------------
// Unsupported instruction
// ---------------------------------------------------------------------------

describe('unsupported instructions', () => {
  it('throws on unknown instruction', () => {
    const src = '(CODE.NOOP)';
    assert.throws(() => run(src), /Unsupported instruction: CODE.NOOP/);
  });
});

// ---------------------------------------------------------------------------
// Integration: optimizer_seed.push3-style programs
// ---------------------------------------------------------------------------

describe('integration: bear/bull program', () => {
  it('simple bear/bull program transpiles without error', () => {
    // Simplified version of the optimizer_v3.push3 structure
    const src = `(
      PERCENTAGESTAKED DYADIC.DEFINE
      TAXRATE DYADIC.DEFINE
      DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
      DYADIC.POP DYADIC.POP

      PERCENTAGESTAKED 100 DYADIC.* 1000000000000000000 DYADIC./
      STAKED DYADIC.DEFINE

      STAKED 91 DYADIC.>
      EXEC.IF
      (
        1000000000000000000
        20
        1000000000000000000
        0
      )
      (
        300000000000000000
        100
        300000000000000000
        0
      )
    )`;
    const result = run(src);
    // Should produce valid output with if/else
    const body = result.functionBody.join('\n');
    assert.ok(body.includes('if ('));
    // Output vars should be result variables (rN) since branches differ
    assert.ok(result.ciVar !== undefined);
    assert.ok(result.functionBody.length > 0);
  });
});
