import { createGatewayRuntimeProviderRegistry } from './agent.module.js';
import { firstValueFrom } from 'rxjs';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
  RuntimeApprovalDeniedError,
  RuntimeProviderService,
} from './runtime-provider-registry.service.js';
import { RuntimeApprovalDeniedFilter } from './runtime-approval-denied.filter.js';
import { InteractionController } from './interaction.controller.js';

describe('InteractionController', (): void => {
  afterEach(() => vi.restoreAllMocks());

  it('maps a denied runtime approval to Fastify HTTP 403', () => {
    const send = vi.fn();
    const status = vi.fn().mockReturnValue({ send });
    const response = { status };
    const host = { switchToHttp: () => ({ getResponse: () => response }) };

    new RuntimeApprovalDeniedFilter().catch(new RuntimeApprovalDeniedError(), host as never);

    expect(status).toHaveBeenCalledWith(403);
    expect(send).toHaveBeenCalledWith({
      statusCode: 403,
      message: 'Runtime termination approval denied',
    });
  });

  it('honors a differently named configured instance without a code change', async () => {
    const prior = process.env['MOSAIC_AGENT_NAME'];
    process.env['MOSAIC_AGENT_NAME'] = 'Nova';
    const runtime = { listSessions: vi.fn().mockResolvedValue([]) };
    const controller = new InteractionController(runtime as never, {} as never);

    await expect(
      controller.sessions('Nova', 'fleet', { id: 'owner', tenantId: 'team' }, 'corr-1'),
    ).resolves.toEqual([]);
    await expect(
      controller.sessions('Other', 'fleet', { id: 'owner', tenantId: 'team' }, 'corr-1'),
    ).rejects.toThrow('Interaction agent is not configured');

    if (prior === undefined) delete process.env['MOSAIC_AGENT_NAME'];
    else process.env['MOSAIC_AGENT_NAME'] = prior;
  });

  it('reaches the registered Hermes provider through the authenticated transitional matrix route', async () => {
    process.env['MOSAIC_AGENT_NAME'] = 'Nova';
    const registry = createGatewayRuntimeProviderRegistry();
    const runtime = new RuntimeProviderService(
      registry,
      { record: vi.fn().mockResolvedValue(undefined) },
      { consume: vi.fn().mockResolvedValue(false) },
    );
    const controller = new InteractionController(runtime, {} as never);

    await expect(
      controller.transitionalCapabilities(
        'Nova',
        'runtime.hermes',
        { id: 'owner', tenantId: 'team' },
        'corr-1',
      ),
    ).resolves.toEqual([
      { capability: 'kanban', status: 'unsupported' },
      { capability: 'skills', status: 'unsupported' },
      { capability: 'memory', status: 'unsupported' },
      { capability: 'tools', status: 'unsupported' },
      { capability: 'cron', status: 'unsupported' },
    ]);
  });

  it('rejects a request without the non-simple correlation header', async () => {
    process.env['MOSAIC_AGENT_NAME'] = 'Nova';
    const controller = new InteractionController({ listSessions: vi.fn() } as never, {} as never);

    await expect(controller.sessions('Nova', 'fleet', { id: 'owner' })).rejects.toThrow(
      'X-Correlation-Id is required',
    );
  });

  it('enrolls a visible runtime session under the cross-surface conversation handle', async () => {
    process.env['MOSAIC_AGENT_NAME'] = 'Nova';
    const runtime = {
      listSessions: vi.fn().mockResolvedValue([{ id: 'runtime-1' }]),
    };
    const durable = { enroll: vi.fn().mockResolvedValue(undefined) };
    const controller = new InteractionController(runtime as never, durable as never);

    await expect(
      controller.enroll(
        'Nova',
        'conversation-1',
        { providerId: 'fleet', runtimeSessionId: 'runtime-1' },
        { id: 'owner', tenantId: 'team' },
        'corr-1',
      ),
    ).resolves.toEqual({ status: 'enrolled', sessionId: 'conversation-1' });
    expect(durable.enroll).toHaveBeenCalledWith(
      {
        agentName: 'Nova',
        sessionId: 'conversation-1',
        tenantId: 'team',
        ownerId: 'owner',
        providerId: 'fleet',
        runtimeSessionId: 'runtime-1',
      },
      expect.objectContaining({ correlationId: 'corr-1' }),
    );
  });

  it('rejects an invalid attach mode before invoking a provider', async () => {
    process.env['MOSAIC_AGENT_NAME'] = 'Nova';
    const controller = new InteractionController({ attach: vi.fn() } as never, {} as never);

    await expect(
      controller.attach('Nova', 'durable-1', { mode: 'write' as never }, { id: 'owner' }, 'corr-1'),
    ).rejects.toThrow('Interaction attach mode is invalid');
  });

  it('resumes a durable session by attaching and streaming its runtime events', async () => {
    process.env['MOSAIC_AGENT_NAME'] = 'Nova';
    const runtimeEvent = {
      type: 'message.delta' as const,
      sessionId: 'runtime-1',
      cursor: 'cursor-1',
      occurredAt: '2026-07-13T00:00:00.000Z',
      content: 'resumed',
    };
    const runtime = {
      attach: vi.fn().mockResolvedValue({ attachmentId: 'attach-1', sessionId: 'runtime-1' }),
      streamSession: vi.fn(async function* () {
        yield runtimeEvent;
      }),
    };
    const durable = {
      getSnapshot: vi.fn().mockResolvedValue({
        identity: { agentName: 'Nova', providerId: 'fleet', runtimeSessionId: 'runtime-1' },
      }),
    };
    const controller = new InteractionController(runtime as never, durable as never);

    await controller.attach('Nova', 'conversation-1', { mode: 'read' }, { id: 'owner' }, 'corr-1');
    await expect(
      firstValueFrom(
        controller.stream('Nova', 'conversation-1', undefined, { id: 'owner' }, 'corr-1'),
      ),
    ).resolves.toEqual({ data: runtimeEvent });

    expect(runtime.attach).toHaveBeenCalledWith(
      'fleet',
      'runtime-1',
      'read',
      expect.objectContaining({ correlationId: 'corr-1' }),
    );
    expect(runtime.streamSession).toHaveBeenCalledWith(
      'fleet',
      'runtime-1',
      undefined,
      expect.objectContaining({ correlationId: 'corr-1' }),
    );
  });

  it('does not create a runtime stream after the SSE subscriber disconnects during snapshot lookup', async () => {
    process.env['MOSAIC_AGENT_NAME'] = 'Nova';
    let resolveSnapshot!: (value: { identity: Record<string, string> }) => void;
    const snapshot = new Promise<{ identity: Record<string, string> }>((resolve) => {
      resolveSnapshot = resolve;
    });
    const runtime = { streamSession: vi.fn() };
    const durable = { getSnapshot: vi.fn().mockReturnValue(snapshot) };
    const controller = new InteractionController(runtime as never, durable as never);

    const subscription = controller
      .stream('Nova', 'conversation-1', undefined, { id: 'owner' }, 'corr-1')
      .subscribe();
    subscription.unsubscribe();
    resolveSnapshot({
      identity: { agentName: 'Nova', providerId: 'fleet', runtimeSessionId: 'runtime-1' },
    });
    await new Promise((resolve) => setTimeout(resolve, 0));

    expect(runtime.streamSession).not.toHaveBeenCalled();
  });

  it.each([
    ['wrong actor', { getSnapshot: vi.fn().mockRejectedValue(new Error('scope mismatch')) }],
    [
      'session-agent mismatch',
      {
        getSnapshot: vi.fn().mockResolvedValue({
          identity: { agentName: 'Other', providerId: 'fleet', runtimeSessionId: 'runtime-1' },
        }),
      },
    ],
  ])('denies a CLI stop for %s', async (_reason, durable) => {
    process.env['MOSAIC_AGENT_NAME'] = 'Nova';
    const runtime = { terminate: vi.fn().mockResolvedValue(undefined) };
    const controller = new InteractionController(runtime as never, durable as never);

    await expect(
      controller.stop(
        'Nova',
        'durable-1',
        { approvalRef: 'approval-1' },
        { id: 'owner' },
        'corr-1',
      ),
    ).rejects.toBeDefined();
    expect(runtime.terminate).not.toHaveBeenCalled();
  });

  it('surfaces a denied runtime approval to the CLI interaction surface', async () => {
    process.env['MOSAIC_AGENT_NAME'] = 'Nova';
    const runtime = {
      terminate: vi.fn().mockRejectedValue(new Error('Runtime termination approval denied')),
    };
    const durable = {
      getSnapshot: vi.fn().mockResolvedValue({
        identity: { agentName: 'Nova', providerId: 'fleet', runtimeSessionId: 'runtime-1' },
      }),
    };
    const controller = new InteractionController(runtime as never, durable as never);

    await expect(
      controller.stop(
        'Nova',
        'durable-1',
        { approvalRef: 'approval-1' },
        { id: 'owner' },
        'corr-1',
      ),
    ).rejects.toThrow('Runtime termination approval denied');
  });

  it('uses the durable session identity and runtime registry for an approved stop', async () => {
    process.env['MOSAIC_AGENT_NAME'] = 'Nova';
    const runtime = { terminate: vi.fn().mockResolvedValue(undefined) };
    const durable = {
      getSnapshot: vi.fn().mockResolvedValue({
        identity: { agentName: 'Nova', providerId: 'fleet', runtimeSessionId: 'runtime-1' },
      }),
    };
    const controller = new InteractionController(runtime as never, durable as never);

    await controller.stop(
      'Nova',
      'durable-1',
      { approvalRef: 'approval-1' },
      { id: 'owner' },
      'corr-1',
    );

    expect(runtime.terminate).toHaveBeenCalledWith(
      'fleet',
      'runtime-1',
      'approval-1',
      expect.objectContaining({
        correlationId: 'corr-1',
        actorScope: { userId: 'owner', tenantId: 'owner' },
      }),
    );
  });
});
