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

const GlmtTransformer = require('../dist/glmt/glmt-transformer').default;
const { DeltaAccumulator } = require('../dist/glmt/delta-accumulator');

/**
 * Token Counting Validation Tests
 *
 * Verifies:
 * 1. message_delta includes both input_tokens and output_tokens
 * 2. Token counts with simple prompts (no tools)
 * 3. Token counts with tool calls
 * 4. Token counts with thinking blocks + tools
 * 5. Deferred finalization waits for usage data
 * 6. Finalization happens when BOTH finish_reason AND usage are received
 * 7. Graceful degradation if usage never arrives
 * 8. No regressions in existing features
 */

class TestRunner {
  constructor() {
    this.tests = [];
    this.passed = 0;
    this.failed = 0;
  }

  test(name, fn) {
    this.tests.push({ name, fn });
  }

  async run() {
    console.log('\n=== Token Counting Validation Tests ===\n');

    for (const { name, fn } of this.tests) {
      try {
        await fn();
        console.log(`✓ ${name}`);
        this.passed++;
      } catch (error) {
        console.error(`✗ ${name}`);
        console.error(`  Error: ${error.message}`);
        if (error.stack) {
          console.error(`  Stack: ${error.stack.split('\n').slice(1, 3).join('\n')}`);
        }
        this.failed++;
      }
    }

    console.log(`\n=== Results ===`);
    console.log(`Passed: ${this.passed}/${this.tests.length}`);
    console.log(`Failed: ${this.failed}/${this.tests.length}`);

    return this.failed === 0;
  }
}

// Assertion helpers
function assertEqual(actual, expected, message) {
  if (actual !== expected) {
    throw new Error(
      `${message || 'Assertion failed'}\n` +
      `  Expected: ${JSON.stringify(expected)}\n` +
      `  Actual: ${JSON.stringify(actual)}`
    );
  }
}

function assertTrue(value, message) {
  if (!value) {
    throw new Error(message || 'Expected true');
  }
}

function assertExists(value, message) {
  if (value === undefined || value === null) {
    throw new Error(message || 'Value should exist');
  }
}

function assertDeepEqual(actual, expected, message) {
  const actualStr = JSON.stringify(actual);
  const expectedStr = JSON.stringify(expected);
  if (actualStr !== expectedStr) {
    throw new Error(
      `${message || 'Deep equality failed'}\n` +
      `  Expected: ${expectedStr}\n` +
      `  Actual: ${actualStr}`
    );
  }
}

const runner = new TestRunner();

// ========================================
// Test 1: message_delta includes input_tokens and output_tokens
// ========================================
runner.test('message_delta includes both input_tokens and output_tokens', () => {
  const transformer = new GlmtTransformer();
  const accumulator = new DeltaAccumulator();

  // Simulate usage data
  accumulator.updateUsage({
    prompt_tokens: 150,
    completion_tokens: 75
  });
  accumulator.finishReason = 'stop';

  const events = transformer.finalizeDelta(accumulator);

  // Find message_delta event
  const messageDelta = events.find(e => e.event === 'message_delta');
  assertExists(messageDelta, 'message_delta event should exist');
  assertExists(messageDelta.data.usage, 'usage should exist in message_delta');
  assertEqual(messageDelta.data.usage.input_tokens, 150, 'input_tokens should be 150');
  assertEqual(messageDelta.data.usage.output_tokens, 75, 'output_tokens should be 75');
});

// ========================================
// Test 2: Token counting with simple prompts (no tools)
// ========================================
runner.test('token counting with simple prompt (no tools)', () => {
  const transformer = new GlmtTransformer();
  const openaiResponse = {
    id: 'chatcmpl-123',
    model: 'GLM-4.6',
    choices: [{
      message: {
        role: 'assistant',
        content: 'Simple response'
      },
      finish_reason: 'stop'
    }],
    usage: {
      prompt_tokens: 10,
      completion_tokens: 5,
      total_tokens: 15
    }
  };

  const result = transformer.transformResponse(openaiResponse, {});

  assertExists(result.usage, 'usage should exist');
  assertEqual(result.usage.input_tokens, 10, 'input_tokens should be 10');
  assertEqual(result.usage.output_tokens, 5, 'output_tokens should be 5');
});

// ========================================
// Test 3: Token counting with tool calls
// ========================================
runner.test('token counting with tool calls', () => {
  const transformer = new GlmtTransformer();
  const openaiResponse = {
    id: 'chatcmpl-456',
    model: 'GLM-4.6',
    choices: [{
      message: {
        role: 'assistant',
        content: null,
        tool_calls: [{
          id: 'call_1',
          type: 'function',
          function: {
            name: 'get_weather',
            arguments: '{"location":"London"}'
          }
        }]
      },
      finish_reason: 'tool_calls'
    }],
    usage: {
      prompt_tokens: 50,
      completion_tokens: 25,
      total_tokens: 75
    }
  };

  const result = transformer.transformResponse(openaiResponse, {});

  assertExists(result.usage, 'usage should exist');
  assertEqual(result.usage.input_tokens, 50, 'input_tokens should be 50');
  assertEqual(result.usage.output_tokens, 25, 'output_tokens should be 25');
  assertEqual(result.stop_reason, 'tool_use', 'stop_reason should be tool_use');
  assertTrue(result.content.some(b => b.type === 'tool_use'), 'should have tool_use block');
});

// ========================================
// Test 4: Token counting with thinking blocks + tools
// ========================================
runner.test('token counting with thinking blocks and tool calls', () => {
  const transformer = new GlmtTransformer();
  const openaiResponse = {
    id: 'chatcmpl-789',
    model: 'GLM-4.6',
    choices: [{
      message: {
        role: 'assistant',
        reasoning_content: 'Let me analyze this request...',
        content: 'I need to call a tool',
        tool_calls: [{
          id: 'call_2',
          type: 'function',
          function: {
            name: 'calculate',
            arguments: '{"expression":"2+2"}'
          }
        }]
      },
      finish_reason: 'tool_calls'
    }],
    usage: {
      prompt_tokens: 100,
      completion_tokens: 80,
      total_tokens: 180
    }
  };

  const result = transformer.transformResponse(openaiResponse, {});

  assertExists(result.usage, 'usage should exist');
  assertEqual(result.usage.input_tokens, 100, 'input_tokens should be 100');
  assertEqual(result.usage.output_tokens, 80, 'output_tokens should be 80');
  assertTrue(result.content.some(b => b.type === 'thinking'), 'should have thinking block');
  assertTrue(result.content.some(b => b.type === 'tool_use'), 'should have tool_use block');
});

// ========================================
// Test 5: Deferred finalization waits for usage data
// ========================================
runner.test('deferred finalization waits for usage data', () => {
  const transformer = new GlmtTransformer();
  const accumulator = new DeltaAccumulator();

  // Simulate finish_reason arriving first
  accumulator.finishReason = 'stop';
  accumulator.messageStarted = true;

  // Usage hasn't arrived yet - should NOT finalize
  assertEqual(accumulator.usageReceived, false, 'usageReceived should be false initially');

  // Simulate transformDelta with finish_reason but no usage
  const openaiEvent1 = {
    event: 'data',
    data: {
      choices: [{
        delta: {},
        finish_reason: 'stop'
      }]
    }
  };

  const events1 = transformer.transformDelta(openaiEvent1, accumulator);

  // Should NOT have message_stop event yet
  const hasMessageStop1 = events1.some(e => e.event === 'message_stop');
  assertEqual(hasMessageStop1, false, 'should NOT finalize without usage');
  assertEqual(accumulator.finalized, false, 'accumulator should NOT be finalized');

  // Now usage arrives
  const openaiEvent2 = {
    event: 'data',
    data: {
      usage: {
        prompt_tokens: 200,
        completion_tokens: 100
      }
    }
  };

  const events2 = transformer.transformDelta(openaiEvent2, accumulator);

  // Should NOW finalize since we have both finish_reason AND usage
  const hasMessageStop2 = events2.some(e => e.event === 'message_stop');
  assertEqual(hasMessageStop2, true, 'should finalize when usage arrives');
  assertEqual(accumulator.finalized, true, 'accumulator should be finalized');
  assertEqual(accumulator.usageReceived, true, 'usageReceived should be true');
});

// ========================================
// Test 6: Finalization happens when BOTH finish_reason AND usage received
// ========================================
runner.test('finalization waits for BOTH finish_reason AND usage', () => {
  const transformer = new GlmtTransformer();
  const accumulator = new DeltaAccumulator();
  accumulator.messageStarted = true;

  // Test case A: Usage arrives first
  const openaiEvent1 = {
    event: 'data',
    data: {
      usage: {
        prompt_tokens: 50,
        completion_tokens: 30
      }
    }
  };

  const events1 = transformer.transformDelta(openaiEvent1, accumulator);
  assertEqual(accumulator.usageReceived, true, 'usage should be received');
  assertEqual(accumulator.finalized, false, 'should NOT finalize with only usage');

  // Test case B: finish_reason arrives second
  const openaiEvent2 = {
    event: 'data',
    data: {
      choices: [{
        delta: {},
        finish_reason: 'stop'
      }]
    }
  };

  const events2 = transformer.transformDelta(openaiEvent2, accumulator);
  assertEqual(accumulator.finishReason, 'stop', 'finish_reason should be set');
  assertEqual(accumulator.finalized, true, 'should finalize when both present');

  const messageDelta = events2.find(e => e.event === 'message_delta');
  assertExists(messageDelta, 'message_delta should exist');
  assertEqual(messageDelta.data.usage.input_tokens, 50, 'input_tokens in message_delta');
  assertEqual(messageDelta.data.usage.output_tokens, 30, 'output_tokens in message_delta');
});

// ========================================
// Test 7: Graceful degradation if usage never arrives
// ========================================
runner.test('graceful degradation when usage never arrives', () => {
  const transformer = new GlmtTransformer();
  const accumulator = new DeltaAccumulator();
  accumulator.messageStarted = true;

  // finish_reason arrives
  accumulator.finishReason = 'stop';

  // Simulate [DONE] event without usage
  const doneEvent = {
    event: 'done'
  };

  const events = transformer.transformDelta(doneEvent, accumulator);

  // Should finalize with zero tokens (graceful degradation)
  assertEqual(accumulator.finalized, true, 'should finalize on [DONE]');
  const messageDelta = events.find(e => e.event === 'message_delta');
  assertExists(messageDelta, 'message_delta should exist');
  assertEqual(messageDelta.data.usage.input_tokens, 0, 'input_tokens should be 0');
  assertEqual(messageDelta.data.usage.output_tokens, 0, 'output_tokens should be 0');
});

// ========================================
// Test 8: No regression - thinking blocks still work
// ========================================
runner.test('no regression: thinking blocks still work', () => {
  const transformer = new GlmtTransformer();
  const accumulator = new DeltaAccumulator();

  // Start message
  const event1 = {
    event: 'data',
    data: {
      model: 'GLM-4.6',
      choices: [{
        delta: { role: 'assistant' }
      }]
    }
  };
  transformer.transformDelta(event1, accumulator);

  // Thinking delta
  const event2 = {
    event: 'data',
    data: {
      choices: [{
        delta: {
          reasoning_content: 'Analyzing the problem...'
        }
      }]
    }
  };
  const events2 = transformer.transformDelta(event2, accumulator);

  // Check thinking block was created
  const hasThinkingStart = events2.some(e =>
    e.event === 'content_block_start' &&
    e.data.content_block.type === 'thinking'
  );
  assertEqual(hasThinkingStart, true, 'thinking block should start');

  const hasThinkingDelta = events2.some(e =>
    e.event === 'content_block_delta' &&
    e.data.delta.type === 'thinking_delta'
  );
  assertEqual(hasThinkingDelta, true, 'thinking delta should be emitted');
});

// ========================================
// Test 9: No regression - tool calls execute correctly
// ========================================
runner.test('no regression: tool calls execute correctly', () => {
  const transformer = new GlmtTransformer();
  const accumulator = new DeltaAccumulator();
  accumulator.messageStarted = true;

  // Tool call delta
  const event = {
    event: 'data',
    data: {
      choices: [{
        delta: {
          tool_calls: [{
            index: 0,
            id: 'call_abc',
            type: 'function',
            function: {
              name: 'search',
              arguments: '{"q":"test"}'
            }
          }]
        }
      }]
    }
  };

  const events = transformer.transformDelta(event, accumulator);

  const toolUseStart = events.find(e =>
    e.event === 'content_block_start' &&
    e.data.content_block.type === 'tool_use'
  );
  assertExists(toolUseStart, 'tool_use block should start');
  assertEqual(toolUseStart.data.content_block.name, 'search', 'tool name should be search');

  const inputJsonDelta = events.find(e =>
    e.event === 'content_block_delta' &&
    e.data.delta.type === 'input_json_delta'
  );
  assertExists(inputJsonDelta, 'input_json_delta should be emitted');
});

// ========================================
// Test 10: No regression - streaming still works
// ========================================
runner.test('no regression: streaming still works', () => {
  const transformer = new GlmtTransformer();
  const accumulator = new DeltaAccumulator();

  // Message start
  const event1 = {
    event: 'data',
    data: {
      model: 'GLM-4.6',
      choices: [{ delta: { role: 'assistant' } }]
    }
  };
  const events1 = transformer.transformDelta(event1, accumulator);
  assertTrue(events1.some(e => e.event === 'message_start'), 'message_start event');

  // Text delta
  const event2 = {
    event: 'data',
    data: {
      choices: [{ delta: { content: 'Hello' } }]
    }
  };
  const events2 = transformer.transformDelta(event2, accumulator);
  assertTrue(events2.some(e => e.event === 'content_block_start'), 'content_block_start');
  assertTrue(events2.some(e => e.event === 'content_block_delta'), 'content_block_delta');

  // More text
  const event3 = {
    event: 'data',
    data: {
      choices: [{ delta: { content: ' world' } }]
    }
  };
  const events3 = transformer.transformDelta(event3, accumulator);
  const textDelta = events3.find(e => e.data?.delta?.type === 'text_delta');
  assertExists(textDelta, 'text_delta should exist');
  assertEqual(textDelta.data.delta.text, ' world', 'delta text should be " world"');
});

// ========================================
// Test 11: No regression - buffered mode still works
// ========================================
runner.test('no regression: buffered mode (non-streaming) works', () => {
  const transformer = new GlmtTransformer();

  const openaiResponse = {
    id: 'chatcmpl-buffered',
    model: 'GLM-4.6',
    choices: [{
      message: {
        role: 'assistant',
        reasoning_content: 'Thinking step by step...',
        content: 'Final answer'
      },
      finish_reason: 'stop'
    }],
    usage: {
      prompt_tokens: 20,
      completion_tokens: 15,
      total_tokens: 35
    }
  };

  const result = transformer.transformResponse(openaiResponse, {});

  assertEqual(result.type, 'message', 'type should be message');
  assertEqual(result.role, 'assistant', 'role should be assistant');
  assertTrue(result.content.some(b => b.type === 'thinking'), 'has thinking');
  assertTrue(result.content.some(b => b.type === 'text'), 'has text');
  assertEqual(result.usage.input_tokens, 20, 'input_tokens');
  assertEqual(result.usage.output_tokens, 15, 'output_tokens');
});

// ========================================
// Test 12: usageReceived flag is set correctly
// ========================================
runner.test('usageReceived flag is set when usage data arrives', () => {
  const accumulator = new DeltaAccumulator();

  assertEqual(accumulator.usageReceived, false, 'initial value should be false');

  accumulator.updateUsage({
    prompt_tokens: 100,
    completion_tokens: 50
  });

  assertEqual(accumulator.usageReceived, true, 'should be true after updateUsage');
  assertEqual(accumulator.inputTokens, 100, 'inputTokens should be 100');
  assertEqual(accumulator.outputTokens, 50, 'outputTokens should be 50');
});

// ========================================
// Test 13: Double finalization protection
// ========================================
runner.test('double finalization protection works', () => {
  const transformer = new GlmtTransformer();
  const accumulator = new DeltaAccumulator();
  accumulator.messageStarted = true;
  accumulator.finishReason = 'stop';
  accumulator.updateUsage({ prompt_tokens: 10, completion_tokens: 5 });

  // First finalization
  const events1 = transformer.finalizeDelta(accumulator);
  assertTrue(events1.length > 0, 'should return events on first finalization');
  assertEqual(accumulator.finalized, true, 'should be finalized');

  // Second finalization attempt
  const events2 = transformer.finalizeDelta(accumulator);
  assertEqual(events2.length, 0, 'should return empty array on second call');
});

// ========================================
// Test 14: Token counts in streaming with thinking + text + tools
// ========================================
runner.test('streaming: token counts with thinking + text + tools', () => {
  const transformer = new GlmtTransformer();
  const accumulator = new DeltaAccumulator();

  // Message start
  transformer.transformDelta({
    event: 'data',
    data: {
      model: 'GLM-4.6',
      choices: [{ delta: { role: 'assistant' } }]
    }
  }, accumulator);

  // Thinking
  transformer.transformDelta({
    event: 'data',
    data: {
      choices: [{ delta: { reasoning_content: 'Thinking...' } }]
    }
  }, accumulator);

  // Text
  transformer.transformDelta({
    event: 'data',
    data: {
      choices: [{ delta: { content: 'Answer' } }]
    }
  }, accumulator);

  // Tool call
  transformer.transformDelta({
    event: 'data',
    data: {
      choices: [{
        delta: {
          tool_calls: [{
            index: 0,
            id: 'call_1',
            type: 'function',
            function: { name: 'tool', arguments: '{}' }
          }]
        }
      }]
    }
  }, accumulator);

  // Usage arrives
  transformer.transformDelta({
    event: 'data',
    data: {
      usage: { prompt_tokens: 300, completion_tokens: 200 }
    }
  }, accumulator);

  // finish_reason arrives
  const finalEvents = transformer.transformDelta({
    event: 'data',
    data: {
      choices: [{ delta: {}, finish_reason: 'tool_calls' }]
    }
  }, accumulator);

  // Verify message_delta has correct tokens
  const messageDelta = finalEvents.find(e => e.event === 'message_delta');
  assertExists(messageDelta, 'message_delta should exist');
  assertEqual(messageDelta.data.usage.input_tokens, 300, 'input_tokens should be 300');
  assertEqual(messageDelta.data.usage.output_tokens, 200, 'output_tokens should be 200');
  assertEqual(messageDelta.data.delta.stop_reason, 'tool_use', 'stop_reason should be tool_use');
});

// Run all tests
runner.run().then(success => {
  process.exit(success ? 0 : 1);
}).catch(error => {
  console.error('Test runner error:', error);
  process.exit(1);
});
