import assert from 'node:assert/strict';
import test from 'node:test';
import {
  applyOpenAIChatContinuationToBody,
  extractProviderContinuationFromRawChunk,
  getOpenAIChatAssistantFieldsForHistoryMessage,
  isProviderContinuationForSource,
  mergeProviderContinuation,
  normalizeProviderContinuationOptions,
  rawOpenAIChatChunkHasToolCalls,
  repairOpenAIChatToolResultPairsInBody,
  withProviderContinuationSource,
} from './providerContinuation';

test('extracts OpenAI-compatible reasoning deltas from raw provider chunks', () => {
  const first = extractProviderContinuationFromRawChunk({
    choices: [
      {
        delta: {
          reasoning_content: 'check ',
        },
      },
    ],
  });
  const second = extractProviderContinuationFromRawChunk({
    choices: [
      {
        delta: {
          reasoning_content: 'tools',
        },
      },
    ],
  });

  const merged = mergeProviderContinuation(first, second);

  assert.equal(merged?.openAIChatAssistantFields?.reasoning_content, 'check tools');
  assert.deepEqual(merged?.reasoningParts, [{ text: 'check tools' }]);
});

test('patches OpenAI-compatible assistant tool-call messages with saved continuation fields', () => {
  const body = JSON.stringify({
    model: 'deepseek-v4-flash',
    stream: true,
    messages: [
      { role: 'system', content: 'system' },
      { role: 'user', content: 'inspect the host' },
      {
        role: 'assistant',
        content: '',
        tool_calls: [
          {
            id: 'call_1',
            type: 'function',
            function: { name: 'run_command', arguments: '{}' },
          },
        ],
      },
      { role: 'tool', tool_call_id: 'call_1', content: '{"ok":true}' },
    ],
  });

  const patched = JSON.parse(
    applyOpenAIChatContinuationToBody(body, [
      { reasoning_content: 'need shell context' },
    ]),
  );

  assert.equal(patched.messages[2].reasoning_content, 'need shell context');
});

test('patches the final assistant message after a tool result with saved continuation fields', () => {
  const body = JSON.stringify({
    model: 'deepseek-v4-flash',
    stream: true,
    messages: [
      { role: 'user', content: 'inspect the host' },
      {
        role: 'assistant',
        content: '',
        tool_calls: [
          {
            id: 'call_1',
            type: 'function',
            function: { name: 'run_command', arguments: '{}' },
          },
        ],
      },
      { role: 'tool', tool_call_id: 'call_1', content: '{"ok":true}' },
      { role: 'assistant', content: 'host is healthy' },
      { role: 'user', content: 'continue' },
    ],
  });

  const patched = JSON.parse(
    applyOpenAIChatContinuationToBody(body, [
      { reasoning_content: 'need shell context' },
      { reasoning_content: 'summarize result' },
    ]),
  );

  assert.equal(patched.messages[1].reasoning_content, 'need shell context');
  assert.equal(patched.messages[3].reasoning_content, 'summarize result');
});

test('rebuilds OpenAI-compatible continuation fields from saved thinking for legacy history', () => {
  const source = { providerConfigId: 'deepseek-custom', providerType: 'custom', modelId: 'deepseek-v4-flash' };

  assert.deepEqual(
    getOpenAIChatAssistantFieldsForHistoryMessage(
      {
        thinking: 'legacy visible reasoning',
        providerId: 'custom',
        model: 'deepseek-v4-flash',
      },
      source,
    ),
    { reasoning_content: 'legacy visible reasoning' },
  );
});

test('does not rebuild continuation fields from thinking when provider or model differs', () => {
  const source = { providerConfigId: 'deepseek-custom', providerType: 'custom', modelId: 'deepseek-v4-flash' };

  assert.equal(
    getOpenAIChatAssistantFieldsForHistoryMessage(
      {
        thinking: 'other provider reasoning',
        providerId: 'openai',
        model: 'deepseek-v4-flash',
      },
      source,
    ),
    undefined,
  );
  assert.equal(
    getOpenAIChatAssistantFieldsForHistoryMessage(
      {
        thinking: 'other model reasoning',
        providerId: 'custom',
        model: 'another-model',
      },
      source,
    ),
    undefined,
  );
  assert.equal(
    getOpenAIChatAssistantFieldsForHistoryMessage(
      {
        thinking: 'missing provider metadata',
        model: 'deepseek-v4-flash',
      },
      source,
    ),
    undefined,
  );
  assert.equal(
    getOpenAIChatAssistantFieldsForHistoryMessage(
      {
        thinking: 'missing model metadata',
        providerId: 'custom',
      },
      source,
    ),
    undefined,
  );
});

test('detects OpenAI-compatible tool calls in raw chunks', () => {
  assert.equal(rawOpenAIChatChunkHasToolCalls({
    choices: [
      {
        delta: {
          tool_calls: [
            {
              id: 'call_1',
              type: 'function',
              function: { name: 'run_command', arguments: '{}' },
            },
          ],
        },
      },
    ],
  }), true);

  assert.equal(rawOpenAIChatChunkHasToolCalls({
    choices: [{ delta: { reasoning_content: 'think' } }],
  }), false);
  assert.equal(rawOpenAIChatChunkHasToolCalls('[DONE]'), false);
});

test('merges provider reasoning metadata into the reasoning part it belongs to', () => {
  const merged = mergeProviderContinuation(
    { reasoningParts: [{ text: 'consider options' }] },
    { reasoningParts: [{ text: '', providerOptions: { anthropic: { signature: 'sig-1' } } }] },
  );

  assert.deepEqual(merged?.reasoningParts, [
    {
      text: 'consider options',
      providerOptions: { anthropic: { signature: 'sig-1' } },
    },
  ]);
});

test('normalizes provider metadata without unsafe object keys', () => {
  const unsafeMetadata = JSON.parse('{"google":{"thoughtSignature":"sig-1","__proto__":{"polluted":true},"nested":{"constructor":{"bad":true},"value":"safe"}},"__proto__":{"ignored":true}}');
  const normalized = normalizeProviderContinuationOptions(unsafeMetadata);

  assert.deepEqual(normalized, {
    google: {
      thoughtSignature: 'sig-1',
      nested: { value: 'safe' },
    },
  });
  assert.equal(Object.prototype.hasOwnProperty.call(normalized?.google ?? {}, '__proto__'), false);
});

test('merges equivalent provider options without depending on key order', () => {
  const merged = mergeProviderContinuation(
    { reasoningParts: [{ text: 'one ', providerOptions: { google: { b: 2, a: 1 } } }] },
    { reasoningParts: [{ text: 'two', providerOptions: { google: { a: 1, b: 2 } } }] },
  );

  assert.deepEqual(merged?.reasoningParts, [
    {
      text: 'one two',
      providerOptions: { google: { b: 2, a: 1 } },
    },
  ]);
});

test('cleans nested unsafe provider option keys when merging saved data', () => {
  const unsafeOptions = JSON.parse('{"google":{"nested":{"prototype":{"bad":true},"value":"safe"}}}');
  const merged = mergeProviderContinuation(
    { reasoningParts: [{ text: 'one ', providerOptions: unsafeOptions }] },
    { reasoningParts: [{ text: 'two' }] },
  );

  assert.deepEqual(merged?.reasoningParts, [
    {
      text: 'one ',
      providerOptions: { google: { nested: { value: 'safe' } } },
    },
    { text: 'two' },
  ]);
});

test('tracks continuation source so provider switches do not replay hidden context', () => {
  const source = { providerConfigId: 'deepseek-custom', providerType: 'custom', modelId: 'deepseek-v4-flash' };
  const continuation = withProviderContinuationSource(
    { openAIChatAssistantFields: { reasoning_content: 'think' } },
    source,
  );

  assert.equal(isProviderContinuationForSource(continuation, source), true);
  assert.equal(
    isProviderContinuationForSource(continuation, {
      providerConfigId: 'openai',
      providerType: 'openai',
      modelId: 'gpt-5',
    }),
    false,
  );
});

test('drops old hidden context instead of relabeling it when sources differ', () => {
  const deepseek = { providerConfigId: 'deepseek-custom', providerType: 'custom', modelId: 'deepseek-v4-flash' };
  const openai = { providerConfigId: 'openai', providerType: 'openai', modelId: 'gpt-5' };
  const merged = mergeProviderContinuation(
    { source: deepseek, openAIChatAssistantFields: { reasoning_content: 'old' } },
    { source: openai, reasoningParts: [{ text: 'new' }] },
  );

  assert.deepEqual(merged, {
    source: openai,
    reasoningParts: [{ text: 'new' }],
  });
});

test('merges tool-call provider options by tool call id', () => {
  const merged = mergeProviderContinuation(
    { toolCallProviderOptionsById: { call_1: { google: { thoughtSignature: 'sig-1' } } } },
    { toolCallProviderOptionsById: { call_1: { google: { extra: true } } } },
  );

  assert.deepEqual(merged?.toolCallProviderOptionsById, {
    call_1: {
      google: {
        thoughtSignature: 'sig-1',
        extra: true,
      },
    },
  });
});

test('skips plain assistant messages that are not part of a tool loop', () => {
  const body = JSON.stringify({
    stream: true,
    messages: [
      { role: 'assistant', content: 'plain answer' },
      {
        role: 'assistant',
        content: '',
        tool_calls: [
          {
            id: 'call_1',
            type: 'function',
            function: { name: 'run_command', arguments: '{}' },
          },
        ],
      },
    ],
  });

  const patched = JSON.parse(
    applyOpenAIChatContinuationToBody(body, [
      { reasoning_content: 'tool reasoning' },
    ]),
  );

  assert.equal(patched.messages[0].reasoning_content, undefined);
  assert.equal(patched.messages[1].reasoning_content, 'tool reasoning');
});

test('keeps assistant tool-call continuation fields aligned with message order', () => {
  const toolCall = (id: string) => ({
    id,
    type: 'function',
    function: { name: 'run_command', arguments: '{}' },
  });
  const body = JSON.stringify({
    stream: true,
    messages: [
      { role: 'assistant', content: '', tool_calls: [toolCall('call_1')] },
      { role: 'tool', tool_call_id: 'call_1', content: '{"ok":true}' },
      { role: 'assistant', content: '', tool_calls: [toolCall('call_2')] },
    ],
  });

  const patched = JSON.parse(
    applyOpenAIChatContinuationToBody(body, [
      undefined,
      { reasoning_content: 'second reasoning' },
    ]),
  );

  assert.equal(patched.messages[0].reasoning_content, undefined);
  assert.equal(patched.messages[2].reasoning_content, 'second reasoning');
});

test('leaves invalid or unchanged OpenAI-compatible request bodies alone', () => {
  assert.equal(applyOpenAIChatContinuationToBody('{', []), '{');

  const body = JSON.stringify({ stream: true, messages: [{ role: 'user', content: 'hi' }] });
  assert.equal(applyOpenAIChatContinuationToBody(body, [{ reasoning_content: 'unused' }]), body);
});

test('leaves complete OpenAI-compatible tool result pairs unchanged', () => {
  const body = JSON.stringify({
    stream: true,
    messages: [
      {
        role: 'assistant',
        content: '',
        tool_calls: [
          { id: 'call_1', type: 'function', function: { name: 'terminal_execute', arguments: '{}' } },
          { id: 'call_2', type: 'function', function: { name: 'terminal_execute', arguments: '{}' } },
        ],
      },
      { role: 'tool', tool_call_id: 'call_1', content: 'one' },
      { role: 'tool', tool_call_id: 'call_2', content: 'two' },
      { role: 'user', content: 'continue' },
    ],
  });

  assert.equal(repairOpenAIChatToolResultPairsInBody(body), body);
});

test('repairs partial OpenAI-compatible tool result pairs before sending history', () => {
  const body = JSON.stringify({
    stream: true,
    messages: [
      {
        role: 'assistant',
        content: '',
        tool_calls: [
          { id: 'call_1', type: 'function', function: { name: 'terminal_execute', arguments: '{}' } },
          { id: 'call_2', type: 'function', function: { name: 'terminal_execute', arguments: '{}' } },
        ],
      },
      { role: 'tool', tool_call_id: 'call_2', content: 'two' },
      { role: 'user', content: 'continue' },
    ],
  });

  const repaired = JSON.parse(repairOpenAIChatToolResultPairsInBody(body));

  assert.deepEqual(
    repaired.messages[0].tool_calls.map((toolCall: { id: string }) => toolCall.id),
    ['call_2'],
  );
  assert.equal(repaired.messages[1].role, 'tool');
  assert.equal(repaired.messages[1].tool_call_id, 'call_2');
  assert.equal(repaired.messages[2].role, 'user');
});

test('drops orphaned OpenAI-compatible tool calls that have no results', () => {
  const body = JSON.stringify({
    stream: true,
    messages: [
      {
        role: 'assistant',
        content: '',
        tool_calls: [
          { id: 'call_1', type: 'function', function: { name: 'terminal_execute', arguments: '{}' } },
        ],
      },
      { role: 'user', content: 'are you there?' },
    ],
  });

  const repaired = JSON.parse(repairOpenAIChatToolResultPairsInBody(body));

  assert.deepEqual(repaired.messages, [
    { role: 'user', content: 'are you there?' },
  ]);
});
