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

import { MatrixError, MinimalMatrixClient, toMatrixPresence } from '../matrix-client.js';

const jsonResponse = (status: number, body: unknown): Response =>
  new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });

// A fetch mock typed with the (URL, RequestInit?) shape the client actually
// calls, so mock.calls has a proper tuple type under noUncheckedIndexedAccess.
const mkFetch = (impl: (url: URL, init?: RequestInit) => Promise<Response>) => vi.fn(impl);

const cfg = {
  homeserverUrl: 'https://matrix.localhost:8448',
  accessToken: 'as-secret',
  actAsUserId: '@agent-alpha:matrix.localhost',
};

describe('toMatrixPresence', () => {
  it('maps liveness states to native presence EDU values', () => {
    expect(toMatrixPresence('online')).toBe('online');
    expect(toMatrixPresence('away')).toBe('unavailable');
    expect(toMatrixPresence('offline')).toBe('offline');
  });
});

describe('MinimalMatrixClient', () => {
  it('setPresence PUTs native presence and masquerades via user_id', async () => {
    const fetchMock = mkFetch(async () => jsonResponse(200, {}));
    const client = new MinimalMatrixClient(cfg, fetchMock as unknown as typeof fetch);
    await client.setPresence('@agent-alpha:matrix.localhost', 'away', 'hb');

    const [url, init] = fetchMock.mock.calls[0]!;
    const u = new URL((url as URL).toString());
    expect(u.pathname).toBe('/_matrix/client/v3/presence/%40agent-alpha%3Amatrix.localhost/status');
    expect(u.searchParams.get('user_id')).toBe('@agent-alpha:matrix.localhost');
    expect(JSON.parse((init as RequestInit).body as string)).toEqual({
      presence: 'unavailable',
      status_msg: 'hb',
    });
    expect((init as RequestInit).method).toBe('PUT');
  });

  it('sendHeartbeat posts an m.room.message and returns the event_id', async () => {
    const fetchMock = mkFetch(async () => jsonResponse(200, { event_id: '$evt1' }));
    const client = new MinimalMatrixClient(cfg, fetchMock as unknown as typeof fetch);
    const id = await client.sendHeartbeat('!room:matrix.localhost', {
      macp_version: '1.0',
      macp_type: 'presence',
      msgtype: 'mosaic.presence',
      agent: { mxid: cfg.actAsUserId, slug: 'alpha', harness: 'claude-code' },
      ts: 1,
      body: 'alpha online (seq 1)',
      status: 'online',
      seq: 1,
      interval_ms: 1000,
    });
    expect(id).toBe('$evt1');
    const [url] = fetchMock.mock.calls[0]!;
    expect((url as URL).pathname).toContain('/rooms/!room%3Amatrix.localhost/send/m.room.message/');
  });

  it('throws a MatrixError carrying errcode on a non-2xx', async () => {
    const fetchMock = mkFetch(async () =>
      jsonResponse(403, { errcode: 'M_FORBIDDEN', error: 'nope' }),
    );
    const client = new MinimalMatrixClient(cfg, fetchMock as unknown as typeof fetch);
    await expect(client.whoami()).rejects.toMatchObject({
      name: 'MatrixError',
      status: 403,
      errcode: 'M_FORBIDDEN',
    });
    await expect(client.whoami()).rejects.toBeInstanceOf(MatrixError);
  });

  it('readHeartbeats reduces the timeline to the latest beat per agent', async () => {
    // Timeline (dir=b => most-recent first). alpha has two beats; keep highest seq.
    const chunk = [
      {
        sender: '@agent-bravo:matrix.localhost',
        origin_server_ts: 9000,
        content: {
          msgtype: 'mosaic.presence',
          agent: { slug: 'bravo', mxid: '@agent-bravo:matrix.localhost' },
          seq: 5,
          status: 'online',
          ts: 8999,
        },
      },
      {
        sender: '@agent-alpha:matrix.localhost',
        origin_server_ts: 8000,
        content: {
          msgtype: 'mosaic.presence',
          agent: { slug: 'alpha', mxid: '@agent-alpha:matrix.localhost' },
          seq: 12,
          status: 'online',
          ts: 7999,
        },
      },
      {
        // an ordinary chat message must be ignored
        sender: '@human:matrix.localhost',
        origin_server_ts: 7000,
        content: { msgtype: 'm.text', body: 'hi' },
      },
      {
        sender: '@agent-alpha:matrix.localhost',
        origin_server_ts: 6000,
        content: {
          msgtype: 'mosaic.presence',
          agent: { slug: 'alpha', mxid: '@agent-alpha:matrix.localhost' },
          seq: 11,
          status: 'online',
          ts: 5999,
        },
      },
    ];
    const fetchMock = mkFetch(async () => jsonResponse(200, { chunk }));
    const client = new MinimalMatrixClient(cfg, fetchMock as unknown as typeof fetch);
    const obs = await client.readHeartbeats('!room:matrix.localhost');

    const bySlug = Object.fromEntries(obs.map((o) => [o.slug, o]));
    expect(Object.keys(bySlug).sort()).toEqual(['alpha', 'bravo']);
    expect(bySlug.alpha).toMatchObject({ lastSeq: 12, lastSeenTs: 8000 }); // highest seq wins, server ts
    expect(bySlug.bravo).toMatchObject({ lastSeq: 5, lastSeenTs: 9000 });

    const [url] = fetchMock.mock.calls[0]!;
    const u = new URL((url as URL).toString());
    expect(u.searchParams.get('dir')).toBe('b');
  });
});
