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

import { MatrixAdapter, type MatrixAdapterConfig } from './adapter.js';
import type { InboundMessage } from '../types.js';

const mockFetch = vi.fn();

function jsonResponse(body: unknown, status = 200): Response {
  return {
    ok: status >= 200 && status < 300,
    status,
    json: async () => body,
    text: async () => JSON.stringify(body),
  } as Response;
}

describe('MatrixAdapter', () => {
  const baseConfig: MatrixAdapterConfig = {
    homeserverUrl: 'https://matrix.example.org',
    accessToken: 'syt_test_token',
    allowedRoomIds: ['!room1:example.org'],
    requireMention: true,
    syncTimeoutMs: 30_000,
    displayName: 'Flynn',
  };

  let adapter: MatrixAdapter;

  beforeEach(() => {
    vi.stubGlobal('fetch', mockFetch);
    vi.clearAllMocks();
    adapter = new MatrixAdapter(baseConfig);
  });

  afterEach(async () => {
    await adapter.disconnect();
    vi.restoreAllMocks();
  });

  it('has name "matrix"', () => {
    expect(adapter.name).toBe('matrix');
  });

  it('starts as disconnected', () => {
    expect(adapter.status).toBe('disconnected');
  });

  it('connect resolves whoami and sets connected', async () => {
    mockFetch.mockImplementation(async (url: string) => {
      if (url.endsWith('/_matrix/client/v3/account/whoami')) {
        return jsonResponse({ user_id: '@flynn:example.org' });
      }
      if (url.includes('/account_data/m.direct')) {
        return jsonResponse({});
      }
      // /sync long-poll hangs
      if (url.includes('/_matrix/client/v3/sync')) {
        return new Promise<Response>(() => {});
      }
      throw new Error(`Unexpected fetch URL: ${url}`);
    });

    await adapter.connect();
    expect(adapter.status).toBe('connected');

    expect(mockFetch).toHaveBeenCalledWith(
      'https://matrix.example.org/_matrix/client/v3/account/whoami',
      expect.objectContaining({
        method: 'GET',
        headers: expect.objectContaining({
          Authorization: 'Bearer syt_test_token',
        }),
      }),
    );
  });

  it('disconnect is safe when not connected', async () => {
    await adapter.disconnect();
    expect(adapter.status).toBe('disconnected');
  });

  it('send throws when not connected', async () => {
    await expect(adapter.send('!room1:example.org', { text: 'hello' })).rejects.toThrow(
      'Matrix adapter not connected',
    );
  });

  it('send delivers a message via PUT', async () => {
    let syncStarted = false;
    mockFetch.mockImplementation(async (url: string, init?: any) => {
      if (url.endsWith('/_matrix/client/v3/account/whoami')) {
        return jsonResponse({ user_id: '@flynn:example.org' });
      }
      if (url.includes('/account_data/m.direct')) {
        return jsonResponse({});
      }
      if (url.includes('/_matrix/client/v3/sync')) {
        syncStarted = true;
        return new Promise<Response>(() => {});
      }
      if (init?.method === 'PUT' && url.includes('/send/m.room.message/')) {
        const body = JSON.parse(init.body);
        expect(body.msgtype).toBe('m.text');
        expect(body.body).toBe('Hello there');
        return jsonResponse({ event_id: '$sent1' });
      }
      throw new Error(`Unexpected fetch URL: ${url}`);
    });

    await adapter.connect();
    expect(syncStarted).toBe(true);

    await adapter.send('!room1:example.org', { text: 'Hello there' });
  });

  it('inbound message requires mention in non-DM rooms', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    let didSync = false;
    mockFetch.mockImplementation(async (url: string) => {
      if (url.endsWith('/_matrix/client/v3/account/whoami')) {
        return jsonResponse({ user_id: '@flynn:example.org' });
      }
      if (url.includes('/account_data/m.direct')) {
        return jsonResponse({});
      }
      if (url.includes('/_matrix/client/v3/sync')) {
        if (didSync) {
          return new Promise<Response>(() => {});
        }
        didSync = true;
        return jsonResponse({
          next_batch: 's1',
          rooms: {
            join: {
              '!room1:example.org': {
                timeline: {
                  events: [
                    {
                      type: 'm.room.message',
                      event_id: '$e1',
                      sender: '@alice:example.org',
                      origin_server_ts: 1700000000000,
                      content: { msgtype: 'm.text', body: 'hello without mention' },
                    },
                  ],
                },
              },
            },
          },
        });
      }
      throw new Error(`Unexpected fetch URL: ${url}`);
    });

    await adapter.connect();
    await new Promise((r) => setTimeout(r, 0));

    expect(handler).not.toHaveBeenCalled();
  });

  it('DM rooms bypass mention requirement (m.direct from sync)', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    let didSync = false;
    mockFetch.mockImplementation(async (url: string) => {
      if (url.endsWith('/_matrix/client/v3/account/whoami')) {
        return jsonResponse({ user_id: '@flynn:example.org' });
      }
      if (url.includes('/account_data/m.direct')) {
        return jsonResponse({});
      }
      if (url.includes('/_matrix/client/v3/sync')) {
        if (didSync) {
          return new Promise<Response>(() => {});
        }
        didSync = true;
        return jsonResponse({
          next_batch: 's1',
          account_data: {
            events: [
              { type: 'm.direct', content: { '@alice:example.org': ['!room1:example.org'] } },
            ],
          },
          rooms: {
            join: {
              '!room1:example.org': {
                timeline: {
                  events: [
                    {
                      type: 'm.room.message',
                      event_id: '$e1',
                      sender: '@alice:example.org',
                      origin_server_ts: 1700000000000,
                      content: { msgtype: 'm.text', body: 'hello dm no mention' },
                    },
                  ],
                },
              },
            },
          },
        });
      }
      throw new Error(`Unexpected fetch URL: ${url}`);
    });

    await adapter.connect();
    await new Promise((r) => setTimeout(r, 0));

    expect(handler).toHaveBeenCalledTimes(1);
    const msg: InboundMessage = handler.mock.calls[0][0];
    expect(msg.channel).toBe('matrix');
    expect(msg.senderId).toBe('!room1:example.org');
    expect(msg.senderName).toBe('alice');
    expect(msg.text).toBe('hello dm no mention');
  });

  it('inbound message with mention is accepted and mention is stripped', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    let didSync = false;
    mockFetch.mockImplementation(async (url: string) => {
      if (url.endsWith('/_matrix/client/v3/account/whoami')) {
        return jsonResponse({ user_id: '@flynn:example.org' });
      }
      if (url.includes('/account_data/m.direct')) {
        return jsonResponse({});
      }
      if (url.includes('/_matrix/client/v3/sync')) {
        if (didSync) {
          return new Promise<Response>(() => {});
        }
        didSync = true;
        return jsonResponse({
          next_batch: 's1',
          rooms: {
            join: {
              '!room1:example.org': {
                timeline: {
                  events: [
                    {
                      type: 'm.room.message',
                      event_id: '$e1',
                      sender: '@alice:example.org',
                      origin_server_ts: 1700000000000,
                      content: { msgtype: 'm.text', body: '@Flynn Hello there' },
                    },
                  ],
                },
              },
            },
          },
        });
      }
      throw new Error(`Unexpected fetch URL: ${url}`);
    });

    await adapter.connect();
    await new Promise((r) => setTimeout(r, 0));

    expect(handler).toHaveBeenCalledTimes(1);
    const msg: InboundMessage = handler.mock.calls[0][0];
    expect(msg.text).toBe('Hello there');
  });
});
