import { describe, expect, it, vi } from 'vitest';
import type { RuntimeScope } from '@mosaicstack/types';
import {
  MatrixNativeRuntimeTransport,
  type MatrixFetchLike,
} from './matrix-native-runtime-transport.js';

const scope: RuntimeScope = {
  actorId: 'operator-1',
  tenantId: 'tenant-a',
  channelId: 'cli',
  correlationId: 'corr-1',
};

const bindings = [
  {
    id: 'native-1',
    runtimeId: '@native-worker:example.test',
    roomId: '!room:example.test',
    remoteUserId: '@native-worker:example.test',
    state: 'active' as const,
    createdAt: '2026-07-13T00:00:00.000Z',
    updatedAt: '2026-07-13T00:00:00.000Z',
  },
];

function response(
  status: number,
  body: unknown = {},
): ReturnType<MatrixFetchLike> extends Promise<infer T> ? T : never {
  return {
    ok: status >= 200 && status < 300,
    status,
    json: async () => body,
    text: async () => JSON.stringify(body),
  } as never;
}

function transport(fetchImpl: MatrixFetchLike): MatrixNativeRuntimeTransport {
  return new MatrixNativeRuntimeTransport({
    homeserverUrl: 'https://matrix.example.test/base',
    accessToken: 'test-token',
    userId: '@mosaic:example.test',
    sessions: bindings,
    fetchImpl,
  });
}

describe('MatrixNativeRuntimeTransport', (): void => {
  it('uses only configured session-to-room bindings and idempotency-derived Matrix transactions', async (): Promise<void> => {
    const fetchImpl = vi
      .fn<MatrixFetchLike>()
      .mockResolvedValueOnce(response(200, { user_id: '@mosaic:example.test' }))
      .mockResolvedValueOnce(response(200, { event_id: '$sent' }));
    const matrix = transport(fetchImpl);

    await matrix.send('native-1', { content: 'hello', idempotencyKey: 'message-1' }, scope);

    expect(fetchImpl).toHaveBeenNthCalledWith(
      1,
      'https://matrix.example.test/base/_matrix/client/v3/account/whoami',
      expect.objectContaining({
        headers: expect.objectContaining({ Authorization: 'Bearer test-token' }),
      }),
    );
    const [url, init] = fetchImpl.mock.calls[1]!;
    expect(url).toMatch(
      /^https:\/\/matrix\.example\.test\/base\/_matrix\/client\/v3\/rooms\/!room%3Aexample\.test\/send\/m\.room\.message\/mosaic-send-[A-Za-z0-9_-]+$/,
    );
    expect(init?.method).toBe('PUT');
    expect(JSON.parse(init?.body ?? '')).toEqual({
      msgtype: 'm.text',
      body: 'hello',
      'mosaic.runtime.v1': {
        session_id: 'native-1',
        runtime_id: '@native-worker:example.test',
        actor_id: 'operator-1',
        tenant_id: 'tenant-a',
        channel_id: 'cli',
        correlation_id: 'corr-1',
        idempotency_key: 'message-1',
      },
    });
  });

  it('sends a termination only to the bound room with the exact approval reference', async (): Promise<void> => {
    const fetchImpl = vi
      .fn<MatrixFetchLike>()
      .mockResolvedValueOnce(response(200, { user_id: '@mosaic:example.test' }))
      .mockResolvedValueOnce(response(200, { event_id: '$terminated' }));
    const matrix = transport(fetchImpl);

    await matrix.terminate('native-1', 'approval-1', scope);

    const [url, init] = fetchImpl.mock.calls[1]!;
    expect(url).toMatch(
      /^https:\/\/matrix\.example\.test\/base\/_matrix\/client\/v3\/rooms\/!room%3Aexample\.test\/send\/mosaic\.runtime\.terminate\/mosaic-terminate-[A-Za-z0-9_-]+$/,
    );
    expect(JSON.parse(init?.body ?? '')).toEqual({
      session_id: 'native-1',
      runtime_id: '@native-worker:example.test',
      actor_id: 'operator-1',
      tenant_id: 'tenant-a',
      channel_id: 'cli',
      correlation_id: 'corr-1',
      approval_ref: 'approval-1',
    });
  });

  it('fails closed when Matrix whoami does not match the configured native identity', async (): Promise<void> => {
    const fetchImpl = vi
      .fn<MatrixFetchLike>()
      .mockResolvedValue(response(200, { user_id: '@other:example.test' }));
    const matrix = transport(fetchImpl);

    await expect(matrix.listSessions(scope)).rejects.toMatchObject({ code: 'forbidden' });
  });

  it('rejects unbound session IDs without using caller-supplied Matrix room data', async (): Promise<void> => {
    const fetchImpl = vi
      .fn<MatrixFetchLike>()
      .mockResolvedValue(response(200, { user_id: '@mosaic:example.test' }));
    const matrix = transport(fetchImpl);

    await expect(matrix.verifySession('!attacker-room:example.test', scope)).rejects.toMatchObject({
      code: 'not_found',
    });
  });

  it('maps replay-cursor events from only the configured remote identity', async (): Promise<void> => {
    const fetchImpl = vi
      .fn<MatrixFetchLike>()
      .mockResolvedValueOnce(response(200, { user_id: '@mosaic:example.test' }))
      .mockResolvedValueOnce(
        response(200, {
          next_batch: 'next-cursor',
          rooms: {
            join: {
              '!room:example.test': {
                timeline: {
                  events: [
                    {
                      type: 'mosaic.runtime.event',
                      sender: '@intruder:example.test',
                      content: { 'mosaic.runtime.v1': { type: 'message.delta', content: 'nope' } },
                    },
                    {
                      type: 'mosaic.runtime.event',
                      sender: '@native-worker:example.test',
                      origin_server_ts: 1_784_246_400_000,
                      content: {
                        'mosaic.runtime.v1': {
                          session_id: 'native-1',
                          type: 'message.delta',
                          content: 'accepted',
                        },
                      },
                    },
                  ],
                },
              },
            },
          },
        }),
      );
    const matrix = transport(fetchImpl);

    const events = [];
    for await (const event of matrix.stream('native-1', 'prior-cursor', scope)) events.push(event);

    expect(events).toEqual([
      {
        type: 'message.delta',
        sessionId: 'native-1',
        cursor: 'next-cursor',
        occurredAt: '2026-07-17T00:00:00.000Z',
        content: 'accepted',
      },
    ]);
    expect(fetchImpl.mock.calls[1]?.[0]).toContain('since=prior-cursor');
  });
});
