import { describe, it, expect, beforeEach, vi } from 'vitest';
import type { ToolContext } from '../tools/index.js';
import type { Message } from '../../llm/openai-compat.js';
import type { Movement } from './types.js';
import { Conversation } from '../context/conversation.js';
import { TRANSITION_TOOL_NAME, COMPLETE_TOOL_NAME } from './terminal-control.js';

const { getToolDefsMock } = vi.hoisted(() => ({ getToolDefsMock: vi.fn() }));

// Override only getToolDefs; keep every other real export (prepareMovement and
// its transitive deps don't need the real tool registry for this test).
vi.mock('../tools/index.js', async (orig) => ({
  ...(await (orig() as Promise<Record<string, unknown>>)),
  getToolDefs: getToolDefsMock,
}));

import { prepareMovement } from './movement-setup.js';

function mv(over: Partial<Movement> = {}): Movement {
  return {
    name: 'm1',
    edit: false,
    persona: 'p',
    instruction: 'do the thing',
    allowedTools: [],
    rules: [{ condition: 'done', next: 'm2' }],
    ...over,
  };
}

const ctx = { workspacePath: '/ws', editAllowed: true } as ToolContext;

const toolNames = (defs: { function: { name: string } }[]) => defs.map((d) => d.function.name);

describe('prepareMovement', () => {
  beforeEach(() => {
    getToolDefsMock.mockReset();
    getToolDefsMock.mockResolvedValue([]); // no regular tools — focus on assembly
  });

  it('includes the transition tool when the movement has rules', async () => {
    const out = await prepareMovement({
      movement: mv({ rules: [{ condition: 'c', next: 'm2' }] }),
      taskInstruction: 'task', ctx, visitCount: 1, maxVisits: 5,
    });
    expect(toolNames(out.tools)).toContain(TRANSITION_TOOL_NAME);
    expect(toolNames(out.tools)).toContain(COMPLETE_TOOL_NAME);
  });

  it('omits the transition tool when the movement has no rules (terminal)', async () => {
    const out = await prepareMovement({
      movement: mv({ rules: [], defaultNext: 'COMPLETE' }),
      taskInstruction: 'task', ctx, visitCount: 1, maxVisits: 5,
    });
    expect(toolNames(out.tools)).not.toContain(TRANSITION_TOOL_NAME);
    expect(toolNames(out.tools)).toContain(COMPLETE_TOOL_NAME);
  });

  it('without a Conversation returns a [system, user] message pair', async () => {
    const out = await prepareMovement({
      movement: mv(), taskInstruction: 'INSTRUCTION', ctx, visitCount: 1, maxVisits: 5,
    });
    expect(out.messages).toHaveLength(2);
    expect(out.messages[0].role).toBe('system');
    expect(out.messages[1].role).toBe('user');
    expect(out.messages[1].content).toBe('INSTRUCTION');
  });

  it('seeds an empty Conversation (system + user taskInstruction)', async () => {
    const conv = new Conversation();
    const out = await prepareMovement({
      movement: mv(), taskInstruction: 'SEED-TASK', ctx, conversation: conv, visitCount: 1, maxVisits: 5,
    });
    expect(out.messages).toBe(conv.messages); // returns the conversation's own array
    expect(conv.messages[0].role).toBe('system');
    expect(conv.messages.some((m) => m.role === 'user' && m.content === 'SEED-TASK')).toBe(true);
  });

  it('seeds a continuation when priorTurns are supplied', async () => {
    const conv = new Conversation();
    const priorTurns: Message[] = [{ role: 'user', content: 'PRIOR-TURN' }];
    await prepareMovement({
      movement: mv(), taskInstruction: 'NEW-TASK', ctx, conversation: conv, priorTurns, visitCount: 1, maxVisits: 5,
    });
    expect(conv.messages.some((m) => m.content === 'PRIOR-TURN')).toBe(true);
    expect(conv.messages.some((m) => m.content === 'NEW-TASK')).toBe(true);
  });

  it('enters the movement (appends user guidance) on a non-empty Conversation', async () => {
    const conv = new Conversation();
    conv.seed('preamble', 'g1', 'first-task'); // pre-existing turns
    const before = conv.messages.length;
    await prepareMovement({
      movement: mv({ name: 'm2' }), taskInstruction: 'second-task', ctx, conversation: conv, visitCount: 1, maxVisits: 5,
    });
    expect(conv.messages.length).toBe(before + 1); // only the guidance is appended
    const last = conv.messages[conv.messages.length - 1];
    expect(last.role).toBe('user');
    expect(String(last.content)).toContain('現在のステップ: m2');
  });
});
