import { describe, expect, it, vi } from 'vitest';
import {
  InMemoryInteractionCoordinationPort,
  InteractionCoordinationClient,
  type CoordinationScope,
  type InteractionCoordinationAuthorityError,
  type InteractionCoordinationPort,
} from '../index.js';

const scope: CoordinationScope = {
  actorId: 'operator-1',
  tenantId: 'tenant-a',
  correlationId: 'corr-1',
  requesterAgentId: 'Nova',
};

function client(
  port: InteractionCoordinationPort,
  handoffIdFactory: () => string = (): string => 'handoff-1',
): InteractionCoordinationClient {
  return new InteractionCoordinationClient(
    { interactionAgentId: 'Nova', orchestrationAgentId: 'Conductor' },
    port,
    handoffIdFactory,
  );
}

describe('InteractionCoordinationClient', (): void => {
  it('round-trips handoff, observation, and result through the native port with identities as data', async (): Promise<void> => {
    const adapter = new InMemoryInteractionCoordinationPort();
    const coordination = client(adapter);

    await expect(
      coordination.handoff(
        { idempotencyKey: 'handoff-request-1', summary: 'Implement the requested feature' },
        scope,
      ),
    ).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');

    await expect(coordination.observe('handoff-1', scope)).resolves.toMatchObject({
      status: 'completed',
      targetAgentId: 'Conductor',
      activity: expect.arrayContaining([
        expect.objectContaining({ status: 'queued' }),
        expect.objectContaining({ status: 'running' }),
        expect.objectContaining({ status: 'completed' }),
      ]),
    });
    await expect(coordination.result('handoff-1', scope)).resolves.toEqual({
      handoffId: 'handoff-1',
      targetAgentId: 'Conductor',
      status: 'completed',
      correlationId: 'corr-1',
      summary: 'Merged by orchestrator',
    });

    expect(coordination).not.toHaveProperty('dispatch');
    expect(coordination).not.toHaveProperty('assign');
    expect(coordination).not.toHaveProperty('review');
    expect(coordination).not.toHaveProperty('merge');
    expect(coordination).not.toHaveProperty('cancel');
  });

  it('fails closed before delivery when an unconfigured agent requests orchestrator work', async (): Promise<void> => {
    const adapter = new InMemoryInteractionCoordinationPort();

    await expect(
      client(adapter).handoff(
        { idempotencyKey: 'handoff-request-1', summary: 'Implement the requested feature' },
        { ...scope, requesterAgentId: 'Untrusted' },
      ),
    ).rejects.toMatchObject({
      code: 'requester_forbidden',
    } satisfies Partial<InteractionCoordinationAuthorityError>);
  });

  it('rejects self-delegation configuration before constructing a client', (): void => {
    expect(
      (): InteractionCoordinationClient =>
        new InteractionCoordinationClient(
          { interactionAgentId: 'Nova', orchestrationAgentId: 'Nova' },
          new InMemoryInteractionCoordinationPort(),
        ),
    ).toThrow('Interaction and orchestration identities must differ');
  });

  it('rejects whitespace-equivalent self-delegation identities', (): void => {
    expect(
      (): InteractionCoordinationClient =>
        new InteractionCoordinationClient(
          { interactionAgentId: 'Nova ', orchestrationAgentId: 'Nova' },
          new InMemoryInteractionCoordinationPort(),
        ),
    ).toThrow('Interaction and orchestration identities must differ');
  });

  it('does not expose another tenant handoff to observe or result', async (): Promise<void> => {
    const adapter = new InMemoryInteractionCoordinationPort();
    const coordination = client(adapter);
    await coordination.handoff(
      { idempotencyKey: 'handoff-request-1', summary: 'Implement the requested feature' },
      scope,
    );

    const otherTenantScope = { ...scope, tenantId: 'tenant-b' };
    await expect(coordination.observe('handoff-1', otherTenantScope)).rejects.toMatchObject({
      code: 'forbidden',
    });
    await expect(coordination.result('handoff-1', otherTenantScope)).rejects.toMatchObject({
      code: 'forbidden',
    });
  });

  it('bounds native handoff retention by evicting the oldest handoff', async (): Promise<void> => {
    const adapter = new InMemoryInteractionCoordinationPort({ maxHandoffs: 1 });
    const first = client(adapter, (): string => 'handoff-1');
    const second = client(adapter, (): string => 'handoff-2');
    await first.handoff({ idempotencyKey: 'handoff-request-1', summary: 'First request' }, scope);
    await second.handoff({ idempotencyKey: 'handoff-request-2', summary: 'Second request' }, scope);

    await expect(first.observe('handoff-1', scope)).rejects.toMatchObject({ code: 'not_found' });
    await expect(second.observe('handoff-2', scope)).resolves.toMatchObject({ status: 'queued' });
  });

  it('fails closed when a transport reports target drift', async (): Promise<void> => {
    const adapter: InteractionCoordinationPort = {
      handoff: vi.fn(async () => ({
        handoffId: 'handoff-1',
        targetAgentId: 'Unexpected',
        status: 'accepted' as const,
        correlationId: 'corr-1',
      })),
      observe: vi.fn(),
      result: vi.fn(),
    };

    await expect(
      client(adapter).handoff(
        { idempotencyKey: 'handoff-request-1', summary: 'Implement the requested feature' },
        scope,
      ),
    ).rejects.toMatchObject({
      code: 'target_drift',
    } satisfies Partial<InteractionCoordinationAuthorityError>);
  });
});
