import { describe, expect, it, vi } from 'vitest';
import {
  InMemoryInteractionCoordinationPort,
  type InteractionCoordinationPort,
  type Handoff,
} from '@mosaicstack/coord';
import type { RuntimeProviderRequestContext } from '../agent/runtime-provider-registry.service.js';
import {
  InteractionCoordinationService,
  type InteractionCoordinationConfig,
  type InteractionCoordinationGatewayError,
} from './interaction-coordination.service.js';

const context: RuntimeProviderRequestContext = {
  actorScope: { userId: 'operator-1', tenantId: 'tenant-a' },
  channelId: 'cli',
  correlationId: 'corr-1',
};

const config: InteractionCoordinationConfig = {
  interactionAgentId: 'Nova',
  orchestrationAgentId: 'Conductor',
};

function service(
  port: InteractionCoordinationPort = new InMemoryInteractionCoordinationPort(),
  options: {
    config?: InteractionCoordinationConfig;
    handoffIdFactory?: () => string;
  } = {},
): InteractionCoordinationService {
  return new InteractionCoordinationService(
    port,
    options.config ?? config,
    options.handoffIdFactory ?? (() => 'handoff-1'),
  );
}

describe('InteractionCoordinationService authority boundary', (): void => {
  it('derives identity and actor/tenant scope server-side, then round-trips the native adapter', async (): Promise<void> => {
    const adapter = new InMemoryInteractionCoordinationPort();
    const coordination = service(adapter);

    await expect(
      coordination.handoff(
        { idempotencyKey: 'request-1', summary: 'Implement the requested feature' },
        context,
      ),
    ).resolves.toEqual({
      handoffId: 'handoff-1',
      targetAgentId: 'Conductor',
      status: 'queued',
      correlationId: 'corr-1',
    });

    adapter.recordActivity('handoff-1', 'running', 'Orchestrator accepted the request');
    adapter.recordResult('handoff-1', 'completed', 'Merged by orchestrator');

    const followUpContext = { ...context, correlationId: 'corr-2' };
    await expect(coordination.observe('handoff-1', followUpContext)).resolves.toMatchObject({
      targetAgentId: 'Conductor',
      status: 'completed',
    });
    await expect(coordination.result('handoff-1', followUpContext)).resolves.toMatchObject({
      targetAgentId: 'Conductor',
      status: 'completed',
      summary: 'Merged by orchestrator',
    });
  });

  it('fails closed without calling a port when the interaction requester is unconfigured', async (): Promise<void> => {
    const adapter = new InMemoryInteractionCoordinationPort();
    const handoff = vi.spyOn(adapter, 'handoff');
    const coordination = service(adapter, {
      config: { interactionAgentId: '', orchestrationAgentId: 'Conductor' },
    });

    await expect(
      coordination.handoff(
        { idempotencyKey: 'request-1', summary: 'Implement the requested feature' },
        context,
      ),
    ).rejects.toMatchObject({
      code: 'unconfigured_requester',
    } satisfies Partial<InteractionCoordinationGatewayError>);
    expect(handoff).not.toHaveBeenCalled();
  });

  it('rejects self-delegation configuration before delivering work', async (): Promise<void> => {
    const adapter = new InMemoryInteractionCoordinationPort();
    const handoff = vi.spyOn(adapter, 'handoff');
    const coordination = service(adapter, {
      config: { interactionAgentId: 'Nova', orchestrationAgentId: 'Nova' },
    });

    await expect(
      coordination.handoff(
        { idempotencyKey: 'request-1', summary: 'Implement the requested feature' },
        context,
      ),
    ).rejects.toThrow('Interaction and orchestration identities must differ');
    expect(handoff).not.toHaveBeenCalled();
  });

  it('denies cross-tenant observe and result before calling the adapter', async (): Promise<void> => {
    const adapter = new InMemoryInteractionCoordinationPort();
    const observe = vi.spyOn(adapter, 'observe');
    const result = vi.spyOn(adapter, 'result');
    const coordination = service(adapter);
    await coordination.handoff(
      { idempotencyKey: 'request-1', summary: 'Implement the requested feature' },
      context,
    );

    const otherTenant = {
      ...context,
      actorScope: { ...context.actorScope, tenantId: 'tenant-b' },
    };
    await expect(coordination.observe('handoff-1', otherTenant)).rejects.toMatchObject({
      code: 'cross_tenant_forbidden',
    } satisfies Partial<InteractionCoordinationGatewayError>);
    await expect(coordination.result('handoff-1', otherTenant)).rejects.toMatchObject({
      code: 'cross_tenant_forbidden',
    } satisfies Partial<InteractionCoordinationGatewayError>);
    expect(observe).not.toHaveBeenCalled();
    expect(result).not.toHaveBeenCalled();
  });

  it('scopes idempotency by actor and joins concurrent retries without duplicate delivery', async (): Promise<void> => {
    let handoffSequence = 0;
    let release: (() => void) | undefined;
    const delivered = new Promise<void>((resolve: () => void): void => {
      release = resolve;
    });
    const adapter: InteractionCoordinationPort = {
      handoff: vi.fn(async (handoff: Handoff) => {
        await delivered;
        return {
          handoffId: handoff.handoffId,
          targetAgentId: handoff.targetAgentId,
          status: 'queued' as const,
          correlationId: handoff.scope.correlationId,
        };
      }),
      observe: vi.fn(),
      result: vi.fn(),
    };
    const coordination = service(adapter, {
      handoffIdFactory: (): string => `handoff-${++handoffSequence}`,
    });
    const request = { idempotencyKey: 'request-1', summary: 'Implement the requested feature' };

    const first = coordination.handoff(request, context);
    const retry = coordination.handoff(request, context);
    expect(adapter.handoff).toHaveBeenCalledTimes(1);
    release?.();
    await expect(Promise.all([first, retry])).resolves.toEqual([
      expect.objectContaining({ handoffId: 'handoff-1' }),
      expect.objectContaining({ handoffId: 'handoff-1' }),
    ]);

    await expect(
      coordination.handoff(request, {
        ...context,
        actorScope: { ...context.actorScope, userId: 'operator-2' },
      }),
    ).resolves.toMatchObject({ handoffId: 'handoff-2' });
    expect(adapter.handoff).toHaveBeenCalledTimes(2);
  });

  it('rejects idempotency-key payload drift and malformed handoff input before delivery', async (): Promise<void> => {
    const adapter = new InMemoryInteractionCoordinationPort();
    const handoff = vi.spyOn(adapter, 'handoff');
    const coordination = service(adapter);
    await coordination.handoff(
      { idempotencyKey: 'request-1', summary: 'Implement the requested feature' },
      context,
    );

    await expect(
      coordination.handoff({ idempotencyKey: 'request-1', summary: 'Different work' }, context),
    ).rejects.toMatchObject({
      code: 'handoff_conflict',
    } satisfies Partial<InteractionCoordinationGatewayError>);
    await expect(
      coordination.handoff({ idempotencyKey: 'request-2', summary: '' }, context),
    ).rejects.toMatchObject({
      code: 'invalid_request',
    } satisfies Partial<InteractionCoordinationGatewayError>);
    await expect(
      coordination.handoff({ idempotencyKey: 'request-3', summary: 'x'.repeat(2_049) }, context),
    ).rejects.toMatchObject({
      code: 'invalid_request',
    } satisfies Partial<InteractionCoordinationGatewayError>);
    expect(handoff).toHaveBeenCalledTimes(1);
  });

  it('fails closed when the port reports a target that drifts from configuration', async (): Promise<void> => {
    const adapter: InteractionCoordinationPort = {
      handoff: vi.fn(async (handoff: Handoff) => ({
        handoffId: handoff.handoffId,
        targetAgentId: 'Unexpected',
        status: 'accepted' as const,
        correlationId: handoff.scope.correlationId,
      })),
      observe: vi.fn(),
      result: vi.fn(),
    };

    await expect(
      service(adapter).handoff(
        { idempotencyKey: 'request-1', summary: 'Implement the requested feature' },
        context,
      ),
    ).rejects.toMatchObject({ code: 'target_drift' });
  });
});
