import { describe, it, expect } from 'vitest';
import { buildMessagesPayload } from '../inference/index.js';
import type {
  Message,
  MessageRole,
  Project,
  Session,
  ToolCall,
  ToolResult,
} from '../../types/api.js';

// ---- fixtures ---------------------------------------------------------------

function makeSession(overrides: Partial<Session> = {}): Session {
  return {
    id: 'sess',
    project_id: 'proj',
    name: 'test session',
    model: 'test-model',
    system_prompt: '',
    status: 'open',
    created_at: new Date(0).toISOString(),
    updated_at: new Date(0).toISOString(),
    agent_id: null,
    web_search_enabled: null,
    ...overrides,
  };
}

function makeProject(overrides: Partial<Project> = {}): Project {
  return {
    id: 'proj',
    name: 'test project',
    path: '/tmp/proj',
    added_at: new Date(0).toISOString(),
    last_session_id: null,
    status: 'open',
    gitea_remote: null,
    default_system_prompt: '',
    default_web_search_enabled: false,
    ...overrides,
  };
}

let counter = 0;
function makeMessage(
  role: MessageRole,
  content: string,
  overrides: Partial<Message> = {}
): Message {
  counter += 1;
  return {
    id: `m${counter}`,
    session_id: 'sess',
    chat_id: 'chat',
    role,
    content,
    kind: 'message',
    tool_calls: null,
    tool_results: null,
    status: 'complete',
    last_seq: 0,
    tokens_used: null,
    ctx_used: null,
    ctx_max: null,
    started_at: null,
    finished_at: null,
    created_at: new Date(counter * 1000).toISOString(),
    metadata: null,
    ...overrides,
  };
}

// ---- tests ------------------------------------------------------------------

describe('buildMessagesPayload', async () => {
  it('prepends a system prompt containing the project path', async () => {
    const session = makeSession();
    const project = makeProject({ path: '/tmp/my-proj' });
    const result = await buildMessagesPayload(session, project, []);
    expect(result).toHaveLength(1);
    expect(result[0]!.role).toBe('system');
    expect(result[0]!.content).toContain('/tmp/my-proj');
  });

  it('appends session.system_prompt to the system message when set', async () => {
    const session = makeSession({ system_prompt: 'Be terse.' });
    const project = makeProject();
    const result = await buildMessagesPayload(session, project, []);
    expect(result).toHaveLength(1);
    expect(result[0]!.role).toBe('system');
    expect(result[0]!.content).toContain('Be terse.');
  });

  it('returns user/assistant messages in order when no compact marker is present', async () => {
    const session = makeSession();
    const project = makeProject();
    const history: Message[] = [
      makeMessage('user', 'hi'),
      makeMessage('assistant', 'hello'),
      makeMessage('user', 'how are you'),
      makeMessage('assistant', 'great'),
    ];
    const result = await buildMessagesPayload(session, project, history);
    // 1 system + 4 history messages
    expect(result).toHaveLength(5);
    expect(result[0]!.role).toBe('system');
    expect(result[1]).toMatchObject({ role: 'user', content: 'hi' });
    expect(result[2]).toMatchObject({ role: 'assistant', content: 'hello' });
    expect(result[3]).toMatchObject({ role: 'user', content: 'how are you' });
    expect(result[4]).toMatchObject({ role: 'assistant', content: 'great' });
  });

  it('does NOT annotate models when the chat uses a single model', async () => {
    const session = makeSession();
    const project = makeProject();
    const history: Message[] = [
      makeMessage('user', 'hi'),
      makeMessage('assistant', 'hello', { model: 'qwen3.6-35b-a3b-mxfp4' }),
      makeMessage('user', 'again'),
      makeMessage('assistant', 'world', { model: 'qwen3.6-35b-a3b-mxfp4' }),
    ];
    const result = await buildMessagesPayload(session, project, history);
    // 1 system + 4 history — no extra attribution system note.
    expect(result).toHaveLength(5);
    expect(result.filter((m) => m.role === 'system')).toHaveLength(1);
    expect(result[2]).toMatchObject({ role: 'assistant', content: 'hello' });
    expect(result[4]).toMatchObject({ role: 'assistant', content: 'world' });
  });

  it('annotates each assistant turn with its model when the chat mixes models', async () => {
    const session = makeSession();
    const project = makeProject();
    const history: Message[] = [
      makeMessage('user', 'hi'),
      makeMessage('assistant', 'opus reply', { model: 'claude-opus-4-8' }),
      makeMessage('user', 'switch'),
      makeMessage('assistant', 'qwen reply', { model: 'qwen3.6-35b-a3b-mxfp4' }),
    ];
    const result = await buildMessagesPayload(session, project, history);
    // 1 system prompt + 1 attribution note + 4 history rows.
    const systems = result.filter((m) => m.role === 'system');
    expect(systems).toHaveLength(2);
    expect(systems[1]!.content).toContain('square brackets');
    const assistants = result.filter((m) => m.role === 'assistant');
    expect(assistants[0]!.content).toBe('[claude-opus-4-8] opus reply');
    expect(assistants[1]!.content).toBe('[qwen3.6-35b-a3b-mxfp4] qwen reply');
  });

  it('starts from the latest compact marker, emitting it as a system message', async () => {
    const session = makeSession();
    const project = makeProject();
    const history: Message[] = [
      makeMessage('user', 'old1'),
      makeMessage('assistant', 'oldreply1'),
      makeMessage('user', 'old2'),
      makeMessage('assistant', 'compacted summary text', { kind: 'compact' }),
      makeMessage('user', 'new1'),
      makeMessage('assistant', 'newreply1'),
    ];
    const result = await buildMessagesPayload(session, project, history);
    // Expect: leading base-system prompt, then the compact as system, then
    // the user/assistant pair following it.
    expect(result).toHaveLength(4);
    expect(result[0]!.role).toBe('system');
    expect(result[1]).toMatchObject({
      role: 'system',
      content: 'compacted summary text',
    });
    expect(result[2]).toMatchObject({ role: 'user', content: 'new1' });
    expect(result[3]).toMatchObject({ role: 'assistant', content: 'newreply1' });
  });

  it('uses only the most recent compact when multiple are present', async () => {
    const session = makeSession();
    const project = makeProject();
    const history: Message[] = [
      makeMessage('user', 'u1'),
      makeMessage('assistant', 'first compact summary', { kind: 'compact' }),
      makeMessage('user', 'u2'),
      makeMessage('assistant', 'second compact summary', { kind: 'compact' }),
      makeMessage('user', 'u3'),
      makeMessage('assistant', 'final reply'),
    ];
    const result = await buildMessagesPayload(session, project, history);
    // Expect: base system + latest compact as system + the two messages
    // following it. The earlier compact and pre-compact history are dropped.
    expect(result).toHaveLength(4);
    expect(result[0]!.role).toBe('system');
    expect(result[1]).toMatchObject({
      role: 'system',
      content: 'second compact summary',
    });
    expect(result[2]).toMatchObject({ role: 'user', content: 'u3' });
    expect(result[3]).toMatchObject({ role: 'assistant', content: 'final reply' });
    // None of the earlier content should leak through
    const concatenated = result.map((m) => m.content ?? '').join(' ');
    expect(concatenated).not.toContain('first compact summary');
    expect(concatenated).not.toContain('u1');
    expect(concatenated).not.toContain('u2');
  });

  it('skips streaming and cancelled assistant rows', async () => {
    const session = makeSession();
    const project = makeProject();
    const history: Message[] = [
      makeMessage('user', 'hi'),
      makeMessage('assistant', 'partial...', { status: 'streaming' }),
      makeMessage('assistant', 'cancelled fragment', { status: 'cancelled' }),
      makeMessage('assistant', 'final answer'),
    ];
    const result = await buildMessagesPayload(session, project, history);
    // 1 system + 1 user + 1 assistant (only the complete one)
    expect(result).toHaveLength(3);
    expect(result[1]).toMatchObject({ role: 'user', content: 'hi' });
    expect(result[2]).toMatchObject({ role: 'assistant', content: 'final answer' });
  });

  it('round-trips an assistant-with-tool_calls followed by its tool result', async () => {
    const session = makeSession();
    const project = makeProject();
    const toolCall: ToolCall = {
      id: 'call_abc',
      name: 'view_file',
      args: { path: 'src/index.ts' },
    };
    const toolResult: ToolResult = {
      tool_call_id: 'call_abc',
      output: { contents: 'console.log(1)' },
      truncated: false,
    };
    const history: Message[] = [
      makeMessage('user', 'show me the file'),
      makeMessage('assistant', '', { tool_calls: [toolCall] }),
      makeMessage('tool', '', { tool_results: toolResult }),
      makeMessage('assistant', 'here it is'),
    ];
    const result = await buildMessagesPayload(session, project, history);
    // 1 system + 1 user + 1 assistant(tool_calls) + 1 tool + 1 assistant
    expect(result).toHaveLength(5);
    expect(result[1]).toMatchObject({ role: 'user', content: 'show me the file' });
    expect(result[2]!.role).toBe('assistant');
    expect(result[2]!.tool_calls).toBeDefined();
    expect(result[2]!.tool_calls).toHaveLength(1);
    expect(result[2]!.tool_calls![0]).toMatchObject({
      id: 'call_abc',
      type: 'function',
      function: { name: 'view_file' },
    });
    // The OpenAI shape stringifies args.
    expect(result[2]!.tool_calls![0]!.function.arguments).toBe(
      JSON.stringify({ path: 'src/index.ts' })
    );
    // assistant with empty content should be serialized as content: null
    expect(result[2]!.content).toBeNull();
    expect(result[3]).toMatchObject({
      role: 'tool',
      tool_call_id: 'call_abc',
    });
    // Non-string tool output is JSON-stringified.
    expect(result[3]!.content).toBe(JSON.stringify({ contents: 'console.log(1)' }));
    expect(result[4]).toMatchObject({ role: 'assistant', content: 'here it is' });
  });

  it('preserves every tool result across a multi-step tool turn', async () => {
    // Regression anchor (dcp-context-corruption-fix): a multi-step tool turn
    // must deliver every prior step's tool output to the payload. Tool rows
    // carry content='' with the output in tool_results; no pre-processing step
    // may drop them.
    const session = makeSession();
    const project = makeProject();
    const history: Message[] = [
      makeMessage('user', 'read x and y'),
      makeMessage('assistant', '', { tool_calls: [{ id: 'c1', name: 'view_file', args: {} }] }),
      makeMessage('tool', '', { tool_results: { tool_call_id: 'c1', output: 'OUT1', truncated: false } }),
      makeMessage('assistant', '', { tool_calls: [{ id: 'c2', name: 'view_file', args: {} }] }),
      makeMessage('tool', '', { tool_results: { tool_call_id: 'c2', output: 'OUT2', truncated: false } }),
    ];
    const result = await buildMessagesPayload(session, project, history);
    const toolContents = result.filter((m) => m.role === 'tool').map((m) => m.content);
    expect(toolContents).toContain('OUT1');
    expect(toolContents).toContain('OUT2');
    // Both assistant turns retain their tool_calls (not stripped as orphans).
    const assistantsWithCalls = result.filter(
      (m) => m.role === 'assistant' && m.tool_calls && m.tool_calls.length > 0
    );
    expect(assistantsWithCalls).toHaveLength(2);
  });

  it('strips assistant tool_calls when matching tool results are missing', async () => {
    const session = makeSession();
    const project = makeProject();
    const toolCall: ToolCall = {
      id: 'call_orphan',
      name: 'grep',
      args: { pattern: 'foo' },
    };
    const history: Message[] = [
      makeMessage('user', 'search'),
      makeMessage('assistant', 'partial answer', { tool_calls: [toolCall] }),
      makeMessage('assistant', 'final answer'),
    ];
    const result = await buildMessagesPayload(session, project, history);
    // tool_calls stripped from the orphan turn; text content kept.
    expect(result).toHaveLength(4);
    expect(result[1]).toMatchObject({ role: 'user', content: 'search' });
    expect(result[2]).toMatchObject({ role: 'assistant', content: 'partial answer' });
    expect(result[2]!.tool_calls).toBeUndefined();
    expect(result[3]).toMatchObject({ role: 'assistant', content: 'final answer' });
  });

  it('drops tool-call-only assistant rows when tool results never arrived', async () => {
    const session = makeSession();
    const project = makeProject();
    const toolCall: ToolCall = {
      id: 'call_orphan_only',
      name: 'grep',
      args: { pattern: 'foo' },
    };
    const history: Message[] = [
      makeMessage('user', 'search'),
      makeMessage('assistant', '', { tool_calls: [toolCall] }),
      makeMessage('assistant', 'final answer'),
    ];
    const result = await buildMessagesPayload(session, project, history);
    expect(result).toHaveLength(3);
    expect(result[2]).toMatchObject({ role: 'assistant', content: 'final answer' });
  });

  it('skips stray tool rows when the owning assistant tool_calls were stripped', async () => {
    const session = makeSession();
    const project = makeProject();
    const toolCallA: ToolCall = {
      id: 'call_a',
      name: 'grep',
      args: { pattern: 'foo' },
    };
    const toolCallB: ToolCall = {
      id: 'call_b',
      name: 'read',
      args: { path: 'x' },
    };
    const toolResult: ToolResult = {
      tool_call_id: 'call_a',
      output: 'match',
      truncated: false,
    };
    const history: Message[] = [
      makeMessage('user', 'search'),
      makeMessage('assistant', '', { tool_calls: [toolCallA, toolCallB] }),
      makeMessage('tool', '', { tool_results: toolResult }),
      makeMessage('assistant', 'final answer'),
    ];
    const result = await buildMessagesPayload(session, project, history);
    expect(result).toHaveLength(3);
    expect(result.find((m) => m.role === 'tool')).toBeUndefined();
    expect(result[2]).toMatchObject({ role: 'assistant', content: 'final answer' });
  });

  it('skips tool rows with no tool_results', async () => {
    const session = makeSession();
    const project = makeProject();
    const history: Message[] = [
      makeMessage('user', 'do it'),
      makeMessage('tool', '', { tool_results: null }),
      makeMessage('assistant', 'done'),
    ];
    const result = await buildMessagesPayload(session, project, history);
    // 1 system + 1 user + 1 assistant; the empty tool row is dropped.
    expect(result).toHaveLength(3);
    expect(result.find((m) => m.role === 'tool')).toBeUndefined();
  });
});
