/**
 * ChronoMind MCP tools - registration + execution tests.
 *
 * Verifies that all ChronoMind tools are registered with correct schemas,
 * descriptions, and required roles, and that the five new ChronoMind MCP
 * proxies call the expected backend endpoints.
 */

import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';

vi.hoisted(() => {
  process.env.JWT_SECRET ??= 'test-secret';
  process.env.CHRONOMIND_BACKEND_URL ??= 'http://localhost:4011';
  return {};
});

import { getTool, listTools } from '../tools/registry.js';
import type { McpToolRequest } from '../tools/types.js';

// Importing the module causes all registerTool() calls to execute.
import './chronomind-tools.js';

const EXPECTED_TOOLS = [
  'chronomind.timers.create',
  'chronomind.timers.list',
  'chronomind.timers.delete',
  'chronomind.timers.reschedule',
  'chronomind.timers.availability',
  'chronomind.routines.list',
  'chronomind.routines.validate',
  'chronomind.routines.start',
  'chronomind.syncStatus',
  'chronomind.sharedTimers.share',
  'chronomind.households.list',
  'chronomind.agentActions.list',
  'chronomind.agentActions.approve',
  'chronomind.planner.planDay',
  'chronomind.planner.planNl',
];

describe('ChronoMind MCP tools registration', () => {
  it('all 15 tools are registered', () => {
    const all = listTools();
    for (const name of EXPECTED_TOOLS) {
      const found = all.find(t => t.name === name);
      expect(found, `tool ${name} should be registered`).toBeDefined();
    }
  });

  it('every tool requires admin role', () => {
    for (const name of EXPECTED_TOOLS) {
      const tool = getTool(name);
      expect(tool?.requiredRole).toBe('admin');
    }
  });

  it('every tool has a description', () => {
    for (const name of EXPECTED_TOOLS) {
      const tool = getTool(name);
      expect(tool?.description).toBeTruthy();
      expect(tool!.description.length).toBeGreaterThan(10);
    }
  });

  it('chronomind.timers.create requires id + label + type', () => {
    const all = listTools();
    const tool = all.find(t => t.name === 'chronomind.timers.create');
    expect(tool?.inputSchema).toBeDefined();
    const props = tool!.inputSchema.properties as Record<string, unknown>;
    expect(props).toHaveProperty('id');
    expect(props).toHaveProperty('label');
    expect(props).toHaveProperty('type');
  });

  it('chronomind.timers.reschedule has timerId + deltaSeconds + targetTime', () => {
    const all = listTools();
    const tool = all.find(t => t.name === 'chronomind.timers.reschedule');
    const props = tool!.inputSchema.properties as Record<string, unknown>;
    expect(props).toHaveProperty('timerId');
    expect(props).toHaveProperty('deltaSeconds');
    expect(props).toHaveProperty('targetTime');
  });

  it('chronomind.planner.planDay requires date + activities', () => {
    const all = listTools();
    const tool = all.find(t => t.name === 'chronomind.planner.planDay');
    const props = tool!.inputSchema.properties as Record<string, unknown>;
    expect(props).toHaveProperty('date');
    expect(props).toHaveProperty('activities');
    expect(props).toHaveProperty('dayStartHour');
    expect(props).toHaveProperty('dayEndHour');
  });

  it('chronomind.planner.planNl requires text', () => {
    const all = listTools();
    const tool = all.find(t => t.name === 'chronomind.planner.planNl');
    const props = tool!.inputSchema.properties as Record<string, unknown>;
    expect(props).toHaveProperty('text');
    expect(props).toHaveProperty('date');
  });

  it('chronomind.timers.availability has start + end + minSlotMinutes', () => {
    const all = listTools();
    const tool = all.find(t => t.name === 'chronomind.timers.availability');
    const props = tool!.inputSchema.properties as Record<string, unknown>;
    expect(props).toHaveProperty('start');
    expect(props).toHaveProperty('end');
    expect(props).toHaveProperty('minSlotMinutes');
  });
});

const fetchMock = vi.fn();

const req: McpToolRequest = {
  id: 'req_1',
  headers: { authorization: 'Bearer token_1' },
  jwtPayload: { sub: 'user_1', role: 'admin', productId: 'chronomind' },
  log: {
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn(),
  },
};

beforeAll(() => {
  vi.stubGlobal('fetch', fetchMock);
});

afterEach(() => {
  fetchMock.mockReset();
});

describe('chronomind MCP tool execution', () => {
  it('reschedules a timer and records an audit action', async () => {
    fetchMock
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          id: 'aa_1',
          userId: 'user_1',
          productId: 'chronomind',
          actorId: 'mcp-server',
          actorType: 'mcp',
          toolName: 'chronomind.timers.reschedule',
          actionType: 'timer.reschedule',
          state: 'proposed',
          payload: { timerId: 'timer_1', deltaSeconds: 600 },
          createdAt: '2026-03-10T00:00:00.000Z',
        }),
      })
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          id: 'timer_1',
          userId: 'user_1',
          productId: 'chronomind',
          label: 'Team meeting',
          type: 'alarm',
          state: 'active',
          urgency: 'standard',
          targetTime: '2026-03-10T10:10:00.000Z',
          createdAt: '2026-03-10T00:00:00.000Z',
          lastSyncedAt: '2026-03-10T00:10:00.000Z',
          syncVersion: 2,
        }),
      });

    const tool = getTool('chronomind.timers.reschedule');
    const result = await tool?.execute(
      { timerId: 'timer_1', deltaSeconds: 600, syncVersion: 2, reason: 'delayed by 10m' },
      req
    );

    expect(fetchMock).toHaveBeenCalledTimes(2);
    expect(new URL(String(fetchMock.mock.calls[0]?.[0])).pathname).toBe('/api/agent-actions');
    expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toMatchObject({
      actorId: 'mcp-server',
      actorType: 'mcp',
      toolName: 'chronomind.timers.reschedule',
      actionType: 'timer.reschedule',
      reason: 'delayed by 10m',
      payload: { timerId: 'timer_1', deltaSeconds: 600 },
    });
    expect(new URL(String(fetchMock.mock.calls[1]?.[0])).pathname).toBe(
      '/api/timers/timer_1/reschedule'
    );
    expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toMatchObject({
      deltaSeconds: 600,
      syncVersion: 2,
    });
    expect(result).toMatchObject({
      id: 'timer_1',
      label: 'Team meeting',
      syncVersion: 2,
    });
  });

  it('looks up free slots in a time window', async () => {
    fetchMock.mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        slots: [
          {
            start: '2026-03-10T09:00:00.000Z',
            end: '2026-03-10T09:30:00.000Z',
            durationMinutes: 30,
          },
        ],
        totalFreeMinutes: 30,
      }),
    });

    const tool = getTool('chronomind.timers.availability');
    const result = await tool?.execute(
      {
        start: '2026-03-10T09:00:00.000Z',
        end: '2026-03-10T12:00:00.000Z',
        minSlotMinutes: 30,
      },
      req
    );

    expect(fetchMock).toHaveBeenCalledTimes(1);
    const url = new URL(String(fetchMock.mock.calls[0]?.[0]));
    expect(url.pathname).toBe('/api/timers/availability');
    expect(url.searchParams.get('start')).toBe('2026-03-10T09:00:00.000Z');
    expect(url.searchParams.get('end')).toBe('2026-03-10T12:00:00.000Z');
    expect(url.searchParams.get('minSlotMinutes')).toBe('30');
    expect(result).toEqual({
      slots: [
        { start: '2026-03-10T09:00:00.000Z', end: '2026-03-10T09:30:00.000Z', durationMinutes: 30 },
      ],
      totalFreeMinutes: 30,
    });
  });

  it('starts a routine and records an audit action', async () => {
    fetchMock
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          id: 'aa_2',
          userId: 'user_1',
          productId: 'chronomind',
          actorId: 'mcp-server',
          actorType: 'mcp',
          toolName: 'chronomind.routines.start',
          actionType: 'routine.start',
          state: 'proposed',
          payload: { routineId: 'routine_1' },
          createdAt: '2026-03-10T00:00:00.000Z',
        }),
      })
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          id: 'routine_1',
          userId: 'user_1',
          productId: 'chronomind',
          name: 'Morning routine',
          steps: [],
          totalDurationMinutes: 15,
          status: 'active',
          isTemplate: false,
          createdAt: '2026-03-10T00:00:00.000Z',
          lastSyncedAt: '2026-03-10T00:10:00.000Z',
          syncVersion: 3,
        }),
      });

    const tool = getTool('chronomind.routines.start');
    const result = await tool?.execute({ routineId: 'routine_1', reason: 'start the day' }, req);

    expect(fetchMock).toHaveBeenCalledTimes(2);
    expect(new URL(String(fetchMock.mock.calls[0]?.[0])).pathname).toBe('/api/agent-actions');
    expect(new URL(String(fetchMock.mock.calls[1]?.[0])).pathname).toBe(
      '/api/routines/routine_1/start'
    );
    expect(result).toMatchObject({ id: 'routine_1', status: 'active' });
  });

  it('lists agent actions with filters', async () => {
    fetchMock.mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        items: [
          {
            id: 'aa_1',
            userId: 'user_1',
            productId: 'chronomind',
            actorId: 'mcp-server',
            actorType: 'mcp',
            toolName: 'chronomind.timers.reschedule',
            actionType: 'timer.reschedule',
            state: 'proposed',
            payload: {},
            createdAt: '2026-03-10T00:00:00.000Z',
          },
        ],
        total: 1,
      }),
    });

    const tool = getTool('chronomind.agentActions.list');
    const result = await tool?.execute(
      {
        state: 'proposed',
        actorId: 'mcp-server',
        toolName: 'chronomind.timers.reschedule',
        limit: 25,
        offset: 0,
      },
      req
    );

    expect(fetchMock).toHaveBeenCalledTimes(1);
    const url = new URL(String(fetchMock.mock.calls[0]?.[0]));
    expect(url.pathname).toBe('/api/agent-actions');
    expect(url.searchParams.get('state')).toBe('proposed');
    expect(url.searchParams.get('actorId')).toBe('mcp-server');
    expect(url.searchParams.get('toolName')).toBe('chronomind.timers.reschedule');
    expect(result).toEqual({
      items: [
        {
          id: 'aa_1',
          userId: 'user_1',
          productId: 'chronomind',
          actorId: 'mcp-server',
          actorType: 'mcp',
          toolName: 'chronomind.timers.reschedule',
          actionType: 'timer.reschedule',
          state: 'proposed',
          payload: {},
          createdAt: '2026-03-10T00:00:00.000Z',
        },
      ],
      total: 1,
    });
  });

  it('approves a proposed agent action', async () => {
    fetchMock.mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        id: 'aa_1',
        userId: 'user_1',
        productId: 'chronomind',
        actorId: 'mcp-server',
        actorType: 'mcp',
        toolName: 'chronomind.timers.reschedule',
        actionType: 'timer.reschedule',
        state: 'approved',
        payload: {},
        createdAt: '2026-03-10T00:00:00.000Z',
        reviewedAt: '2026-03-10T00:10:00.000Z',
        reviewedBy: 'user_1',
      }),
    });

    const tool = getTool('chronomind.agentActions.approve');
    const result = await tool?.execute({ actionId: 'aa_1' }, req);

    expect(fetchMock).toHaveBeenCalledTimes(1);
    expect(new URL(String(fetchMock.mock.calls[0]?.[0])).pathname).toBe(
      '/api/agent-actions/aa_1/approve'
    );
    expect(result).toMatchObject({ id: 'aa_1', state: 'approved', reviewedBy: 'user_1' });
  });
});
