#!/usr/bin/env node
'use strict';

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');

const {
  normalizeEventLine,
  normalizeEventLines,
} = require('../normalize-events');

const fixturesDir = path.join(__dirname, 'fixtures');
const schemaPath = path.join(__dirname, '..', '..', 'spec', 'events.schema.json');
const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
const dialectEnum = schema.properties.dialect.enum;
const dialectPattern = new RegExp(
  `^(${dialectEnum.map(dialect => dialect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})$`,
);

function readFixture(name) {
  return fs.readFileSync(path.join(fixturesDir, name), 'utf8').trim().split('\n');
}

function assertSchema(event) {
  assert.equal(event.v, 'events/v1');
  assert.equal(typeof event.seq, 'number');
  assert.equal(Number.isInteger(event.seq), true);
  assert.equal(typeof event.position, 'number');
  assert.equal(Number.isInteger(event.position), true);
  assert.match(event.dialect, dialectPattern);
  assert.match(event.kind, /^(message|tool_call|status|error|usage)$/);
  assert.equal(typeof event.raw, 'string');
  if (event.kind === 'error') {
    assert.equal(typeof event.error, 'object');
    assert.equal(typeof event.error.code, 'string');
    assert.equal(typeof event.error.message, 'string');
    if (Object.prototype.hasOwnProperty.call(event.error, 'errorKind')) {
      assert.match(event.error.errorKind, /^(quota|transport|semantic)$/);
    }
  }
  if (event.kind === 'usage') {
    assert.equal(typeof event.usage, 'object');
    assert.equal(typeof event.usage.model, 'string');
    if (event.usage.unknown === true) {
      assert.equal(Object.prototype.hasOwnProperty.call(event.usage, 'inputTokens'), false);
      assert.equal(Object.prototype.hasOwnProperty.call(event.usage, 'outputTokens'), false);
    } else {
      assert.equal(typeof event.usage.inputTokens, 'number');
      assert.equal(typeof event.usage.outputTokens, 'number');
    }
  }
}

test('events schema declares the normalized contract', () => {
  assert.equal(schema.properties.v.const, 'events/v1');
  assert.deepEqual(schema.properties.dialect.enum, dialectEnum);
  assert.deepEqual(schema.properties.kind.enum, ['message', 'tool_call', 'status', 'error', 'usage']);
  assert(schema.required.includes('seq'));
  assert(schema.required.includes('position'));
  assert(schema.required.includes('raw'));
  assert.deepEqual(schema.properties.error.properties.errorKind.enum, ['quota', 'transport', 'semantic']);
  assert.equal(schema.properties.usage.type, 'object');
});

test('normalizeEventLines maps claude dialect lines to events/v1', () => {
  const lines = readFixture('claude.jsonl');
  const events = normalizeEventLines('claude', lines);

  assert.equal(events.length, 3);
  assert.deepEqual(
    events.map(event => ({ kind: event.kind, subtype: event.subtype, seq: event.seq })),
    [
      { kind: 'status', subtype: 'message_start', seq: 0 },
      { kind: 'message', subtype: 'text_delta', seq: 1 },
      { kind: 'status', subtype: 'message_delta', seq: 2 },
    ],
  );
  assert.equal(events[1].text, 'Hello from Claude');
  for (const event of events) {
    assertSchema(event);
  }
});

test('normalizeEventLines maps cursor dialect lines to events/v1', () => {
  const lines = readFixture('cursor.jsonl');
  const events = normalizeEventLines('cursor', lines);

  assert.deepEqual(
    events.map(event => ({ kind: event.kind, subtype: event.subtype })),
    [
      { kind: 'message', subtype: 'message' },
      { kind: 'tool_call', subtype: 'tool_call' },
      { kind: 'status', subtype: 'done' },
    ],
  );
  assert.equal(events[0].role, 'assistant');
  assert.equal(events[0].text, 'Cursor says hi');
  assert.deepEqual(events[1].tool, { name: 'bash', arguments: { command: 'pwd' } });
  for (const event of events) {
    assertSchema(event);
  }
});

test('normalizeEventLines maps codex dialect lines to events/v1', () => {
  const lines = readFixture('codex.jsonl');
  const events = normalizeEventLines('codex', lines);

  assert.deepEqual(
    events.map(event => ({ kind: event.kind, subtype: event.subtype })),
    [
      { kind: 'message', subtype: 'response.output_text.delta' },
      { kind: 'tool_call', subtype: 'response.tool_call' },
      { kind: 'status', subtype: 'response.completed' },
    ],
  );
  assert.equal(events[0].text, 'Codex chunk');
  assert.deepEqual(events[1].tool, { name: 'apply_patch', arguments: '*** Begin Patch' });
  for (const event of events) {
    assertSchema(event);
  }
});

test('normalizeEventLines maps north dialect lines to events/v1', () => {
  const lines = readFixture('claude.jsonl');
  const events = normalizeEventLines('north', lines);

  assert.deepEqual(
    events.map(event => ({ kind: event.kind, subtype: event.subtype })),
    [
      { kind: 'status', subtype: 'message_start' },
      { kind: 'message', subtype: 'text_delta' },
      { kind: 'status', subtype: 'message_delta' },
    ],
  );
  assert.equal(events[1].text, 'Hello from Claude');
  for (const event of events) {
    assertSchema(event);
  }
});

test('normalizeEventLines maps solweb dialect lines to events/v1', () => {
  const lines = readFixture('claude.jsonl');
  const events = normalizeEventLines('solweb', lines);

  assert.deepEqual(
    events.map(event => ({ kind: event.kind, subtype: event.subtype })),
    [
      { kind: 'status', subtype: 'message_start' },
      { kind: 'message', subtype: 'text_delta' },
      { kind: 'status', subtype: 'message_delta' },
    ],
  );
  assert.equal(events[1].text, 'Hello from Claude');
  for (const event of events) {
    assertSchema(event);
  }
});

test('normalizeEventLines maps grok dialect lines to events/v1', () => {
  const lines = readFixture('grok.jsonl');
  const events = normalizeEventLines('grok', lines);

  assert.deepEqual(
    events.map(event => ({ kind: event.kind, subtype: event.subtype })),
    [
      { kind: 'message', subtype: 'text' },
      { kind: 'message', subtype: 'thought' },
      { kind: 'status', subtype: 'end' },
      { kind: 'error', subtype: 'error' },
    ],
  );
  assert.equal(events[0].text, 'Hello from Grok');
  assert.equal(events[1].text, 'checking files');
  assert.equal(events[3].error.code, 'grok_error');
  assert.equal(events[3].error.message, 'auth failed');
  for (const event of events) {
    assertSchema(event);
  }
});

test('normalizeEventLine derives seq directly from position', () => {
  const event = normalizeEventLine('cursor', '{"type":"done"}', 17);
  assert.equal(event.seq, 17);
  assert.equal(event.position, 17);
  assert.equal(event.kind, 'status');
  assertSchema(event);
});

test('normalizeEventLine emits one error event for a garbled line', () => {
  const event = normalizeEventLine('claude', '{"type":"message_start"', 9);

  assert.equal(event.kind, 'error');
  assert.equal(event.seq, 9);
  assert.equal(event.position, 9);
  assert.equal(event.error.code, 'invalid_json');
  assert.match(event.error.message, /JSON|property value|Expected/);
  assertSchema(event);
});

test('usage events satisfy the additive schema', () => {
  assertSchema({
    v: 'events/v1',
    seq: 4,
    position: 4,
    dialect: 'codex',
    kind: 'usage',
    subtype: 'final',
    raw: '{"kind":"usage"}',
    parsed: { kind: 'usage' },
    usage: {
      inputTokens: 12,
      outputTokens: 5,
      cachedTokens: 3,
      costUsd: 0.02,
      model: 'gpt-5.5-high',
    },
  });
  assertSchema({
    v: 'events/v1',
    seq: 5,
    position: 5,
    dialect: 'north',
    kind: 'usage',
    subtype: 'final',
    raw: '{"kind":"usage"}',
    parsed: { kind: 'usage' },
    usage: {
      unknown: true,
      model: 'cohere/north-mini-code:free',
    },
  });
});

test('error events allow optional errorKind', () => {
  assertSchema({
    v: 'events/v1',
    seq: 6,
    position: 6,
    dialect: 'codex',
    kind: 'error',
    subtype: 'error',
    raw: '{"event":"error"}',
    parsed: { event: 'error' },
    error: {
      code: 'rate_limit',
      message: 'quota exhausted',
      errorKind: 'quota',
    },
  });
});

test('normalizeEventLine is stateless per line', () => {
  const first = normalizeEventLine('codex', '{"event":"response.output_text.delta","delta":"A"}', 3);
  const second = normalizeEventLine('codex', '{"event":"response.output_text.delta","delta":"A"}', 3);

  assert.deepEqual(second, first);
  second.parsed.delta = 'B';
  const third = normalizeEventLine('codex', '{"event":"response.output_text.delta","delta":"A"}', 3);
  assert.equal(third.parsed.delta, 'A');
});
