/**
 * MembraneStepper unit tests — Phase 1 tests #1-29
 */
import { describe, it, before } from 'node:test';
import assert from 'node:assert/strict';
import type { OpCall, State } from '@open-matrix/omega-core';
import { MembraneStepper, DEFAULT_MEMBRANE_SOURCE } from '../MembraneStepper.js';
import { MembraneEffectBackend, MembraneResultCollector } from '../MembraneEffectBackend.js';

// ─── Construction tests (#1-4) ──────────────────────────────────────────────

describe('MembraneStepper construction', () => {
  it('#1 create() succeeds without error', () => {
    const stepper = MembraneStepper.create();
    assert.ok(stepper);
  });

  it('#2 create() with custom backends includes those backends', () => {
    const customBackend = {
      ops: ['custom.op'] as readonly string[],
      async handle(_state: State, opcall: OpCall) { return opcall.resumption.invoke({ tag: 'Unit' }); },
    };
    const stepper = MembraneStepper.create([customBackend]);
    assert.ok(stepper);
  });

  it('#3 create() registers all membrane.* effects', () => {
    const stepper = MembraneStepper.create();
    const backend = stepper.getBackend();
    assert.ok(backend);
    // The MembraneEffectBackend registers these ops
    const expectedOps: Array<(typeof backend.ops)[number]> = [
      'membrane.allow',
      'membrane.deny',
      'membrane.trace',
      'membrane.mutate',
      'membrane.log',
      'membrane.rate-check',
    ];
    for (const op of expectedOps) {
      assert.ok(backend.ops.includes(op), `Missing op: ${op}`);
    }
  });

  it('#4 multiple create() calls return independent instances', () => {
    const s1 = MembraneStepper.create();
    const s2 = MembraneStepper.create();
    assert.notStrictEqual(s1, s2);
    assert.notStrictEqual(s1.getBackend(), s2.getBackend());
  });

  it('#4a create() uses kernel primitives instead of extended omega registries', async () => {
    const stepper = MembraneStepper.create();

    const commonProgram = stepper.compile(
      '(begin (string-append "membrane-" "ok") (effect membrane.allow op payload))',
    );
    const commonResult = await stepper.run(commonProgram, {
      op: '$ping',
      payload: {},
      sender: 'a',
      meta: {},
    });
    assert.strictEqual(commonResult.action, 'allow');

    const packageProgram = stepper.compile(
      '(begin (define-package "demo") (effect membrane.allow op payload))',
    );
    const packageResult = await stepper.run(packageProgram, {
      op: '$ping',
      payload: {},
      sender: 'a',
      meta: {},
    });
    assert.strictEqual(packageResult.action, 'deny');
    assert.match(packageResult.error ?? '', /define-package/);
  });

  it('#4b create() keeps closure-based list combinators available on the kernel path', async () => {
    const stepper = MembraneStepper.create();

    const program = stepper.compile(
      '(begin (map (lambda (x) (+ x 1)) (list 1 2 3)) (filter (lambda (x) (> x 1)) (list 1 2 3)) (effect membrane.allow op payload))',
    );
    const result = await stepper.run(program, {
      op: '$ping',
      payload: {},
      sender: 'a',
      meta: {},
    });
    assert.strictEqual(result.action, 'allow');
  });
});

// ─── Compilation tests (#5-9) ───────────────────────────────────────────────

describe('MembraneStepper compilation', () => {
  let stepper: MembraneStepper;

  before(() => {
    stepper = MembraneStepper.create();
  });

  it('#5 compile() with valid Lisp returns an Expr object', () => {
    const expr = stepper.compile('(effect membrane.allow op payload)');
    assert.ok(expr);
    assert.ok(typeof expr === 'object');
    assert.ok('tag' in expr);
  });

  it('#6 compile() with invalid Lisp throws a readable error', () => {
    assert.throws(
      () => stepper.compile('(((invalid lisp'),
      (err: unknown) => {
        assert.ok(err instanceof Error);
        return true;
      },
    );
  });

  it('#7 compile() result is deterministic', () => {
    const expr1 = stepper.compile(DEFAULT_MEMBRANE_SOURCE);
    const expr2 = stepper.compile(DEFAULT_MEMBRANE_SOURCE);
    assert.deepStrictEqual(expr1, expr2);
  });

  it('#8 compile() with default pass-through program succeeds', () => {
    const expr = stepper.compile(DEFAULT_MEMBRANE_SOURCE);
    assert.ok(expr);
    // compile() wraps body in lambda, so the result is a Lambda Expr
    assert.strictEqual(expr.tag, 'Lambda');
  });

  it('#9 pre-compiled program can be reused across multiple run() calls', async () => {
    const program = stepper.compile(DEFAULT_MEMBRANE_SOURCE);
    const r1 = await stepper.run(program, { op: '$ping', payload: {}, sender: 'a', meta: {} });
    const r2 = await stepper.run(program, { op: '$ping', payload: {}, sender: 'b', meta: {} });
    assert.strictEqual(r1.action, 'allow');
    assert.strictEqual(r2.action, 'allow');
  });
});

// ─── Execution — basic (#10-17) ─────────────────────────────────────────────

describe('MembraneStepper execution (basic)', () => {
  let stepper: MembraneStepper;

  before(() => {
    stepper = MembraneStepper.create();
  });

  it('#10 pass-through program returns allow', async () => {
    const program = stepper.compile(DEFAULT_MEMBRANE_SOURCE);
    const result = await stepper.run(program, { op: '$ping', payload: { x: 1 }, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
  });

  it('#11 deny program returns deny with reason', async () => {
    const program = stepper.compile('(effect membrane.deny "BLOCKED")');
    const result = await stepper.run(program, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'deny');
    assert.strictEqual(result.error, 'BLOCKED');
  });

  it('#12 mutate program returns mutate with new payload', async () => {
    const program = stepper.compile('(effect membrane.mutate "{\\"modified\\":true}")');
    const result = await stepper.run(program, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'mutate');
    assert.deepStrictEqual(result.payload, { modified: true });
  });

  it('#13 trace program emits trace AND returns allow', async () => {
    const program = stepper.compile('(begin (effect membrane.trace op sender) (effect membrane.allow op payload))');
    const result = await stepper.run(program, { op: 'test', payload: {}, sender: 'origin', meta: {} });
    assert.strictEqual(result.action, 'allow');
    assert.ok(result.traceData);
    assert.deepStrictEqual(result.traceData, { op: 'test', sender: 'origin', extra: undefined });
  });

  it('#14 passes op, payload, sender, meta bindings into Lisp scope', async () => {
    // Use deny with op as error message to verify op binding
    const program = stepper.compile('(effect membrane.deny op)');
    const result = await stepper.run(program, { op: '$custom-op', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'deny');
    assert.strictEqual(result.error, '$custom-op');
  });

  it('#15 program reads op binding correctly', async () => {
    const program = stepper.compile('(effect membrane.deny op)');
    const result = await stepper.run(program, { op: 'specific-op', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.error, 'specific-op');
  });

  it('#16 program reads payload binding correctly', async () => {
    // Payload is JSON-stringified. Use it as deny error to verify.
    const program = stepper.compile('(effect membrane.deny payload)');
    const result = await stepper.run(program, { op: 'test', payload: { key: 'value' }, sender: 'a', meta: {} });
    assert.strictEqual(result.error, '{"key":"value"}');
  });

  it('#17 program reads sender binding correctly', async () => {
    const program = stepper.compile('(effect membrane.deny sender)');
    const result = await stepper.run(program, { op: 'test', payload: {}, sender: 'system.llm', meta: {} });
    assert.strictEqual(result.error, 'system.llm');
  });
});

// ─── Execution — edge cases (#18-24) ────────────────────────────────────────

describe('MembraneStepper execution (edge cases)', () => {
  let stepper: MembraneStepper;

  before(() => {
    stepper = MembraneStepper.create();
  });

  it('#18 empty payload {} works', async () => {
    const program = stepper.compile(DEFAULT_MEMBRANE_SOURCE);
    const result = await stepper.run(program, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
  });

  it('#19 deeply nested payload (5+ levels) works', async () => {
    const deep = { a: { b: { c: { d: { e: { f: 'deep' } } } } } };
    const program = stepper.compile(DEFAULT_MEMBRANE_SOURCE);
    const result = await stepper.run(program, { op: 'test', payload: deep, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
  });

  it('#20 array payload works', async () => {
    const program = stepper.compile(DEFAULT_MEMBRANE_SOURCE);
    const result = await stepper.run(program, { op: 'test', payload: [1, 2, 3], sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
  });

  it('#21 null/undefined payload defaults gracefully', async () => {
    const program = stepper.compile(DEFAULT_MEMBRANE_SOURCE);
    const r1 = await stepper.run(program, { op: 'test', payload: null, sender: 'a', meta: {} });
    assert.strictEqual(r1.action, 'allow');
    const r2 = await stepper.run(program, { op: 'test', payload: undefined, sender: 'a', meta: {} });
    assert.strictEqual(r2.action, 'allow');
  });

  it('#22 very long op string (1000+ chars) works', async () => {
    const longOp = 'x'.repeat(1200);
    const program = stepper.compile(DEFAULT_MEMBRANE_SOURCE);
    const result = await stepper.run(program, { op: longOp, payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
  });

  it('#23 unicode in payload works', async () => {
    const program = stepper.compile(DEFAULT_MEMBRANE_SOURCE);
    const result = await stepper.run(program, { op: 'test', payload: { msg: '日本語テスト 🎉' }, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
  });

  it('#24 empty string op works without crash', async () => {
    const program = stepper.compile(DEFAULT_MEMBRANE_SOURCE);
    const result = await stepper.run(program, { op: '', payload: {}, sender: 'a', meta: {} });
    // Empty op still goes through default membrane (allow)
    assert.strictEqual(result.action, 'allow');
  });
});

// ─── Error handling (#25-29) ────────────────────────────────────────────────

describe('MembraneStepper error handling', () => {
  let stepper: MembraneStepper;

  before(() => {
    stepper = MembraneStepper.create();
  });

  it('#25 program that throws → returns deny with error reason', async () => {
    // Division by zero or similar error
    const program = stepper.compile('(/ 1 0)');
    const result = await stepper.run(program, { op: 'test', payload: {}, sender: 'a', meta: {} });
    // If division by zero doesn't throw in omega, the result might still be allow (default)
    // or it could return Infinity. Let's test with an explicit error:
    assert.ok(result.action === 'allow' || result.action === 'deny');
  });

  it('#25b explicit error program → returns deny', async () => {
    // Call an undefined function — should trigger error
    const program = stepper.compile('(undefined-function-xyz 1 2 3)');
    const result = await stepper.run(program, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'deny');
    assert.ok(result.error?.includes('Membrane error'));
  });

  it('#26 infinite loop → hits maxSteps limit → returns deny', async () => {
    // Use a small maxSteps to trigger quickly
    const smallStepper = MembraneStepper.create([], { maxSteps: 50 });
    // Self-recursive function that never terminates
    const program = smallStepper.compile('(begin (define (loop) (loop)) (loop))');
    const result = await smallStepper.run(program, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'deny');
    assert.ok(result.error?.includes('Membrane error'));
  });

  it('#27 undefined function → readable error', async () => {
    const program = stepper.compile('(nonexistent-func 42)');
    const result = await stepper.run(program, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'deny');
    assert.ok(result.error);
  });

  it('#28 type error → readable error', async () => {
    // Try to add a string and number
    const program = stepper.compile('(+ "hello" 42)');
    const result = await stepper.run(program, { op: 'test', payload: {}, sender: 'a', meta: {} });
    // Omega might handle this differently — it could coerce or error
    // The point is it should NOT crash the stepper
    assert.ok(result.action === 'allow' || result.action === 'deny');
  });

  it('#29 membrane error NEVER propagates — always caught', async () => {
    // Even the worst program should not throw from run()
    const program = stepper.compile('(car 42)'); // type error: car of non-pair
    const result = await stepper.run(program, { op: 'test', payload: {}, sender: 'a', meta: {} });
    // Should not throw, should return a result
    assert.ok(result);
    assert.ok(['allow', 'deny', 'mutate'].includes(result.action));
  });
});
