import { beforeEach, describe, expect, it, vi } from 'vitest';
import { MattermostAdapter, type MattermostAdapterConfig } 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('MattermostAdapter', () => {
  const baseConfig: MattermostAdapterConfig = {
    serverUrl: 'https://mm.example.com',
    botToken: 'mm-token',
    allowedChannelIds: ['chan-1'],
    requireMention: true,
    mentionName: 'flynn',
    pollIntervalMs: 60_000,
  };

  beforeEach(() => {
    vi.useFakeTimers();
    vi.stubGlobal('fetch', mockFetch);
    vi.clearAllMocks();
  });

  it('sends outbound posts via /api/v4/posts', async () => {
    mockFetch.mockImplementation(async (url: string, init?: RequestInit) => {
      if (url.endsWith('/api/v4/users/me')) {
        return jsonResponse({ id: 'bot-user', username: 'flynnbot' });
      }
      if (url.endsWith('/api/v4/channels/chan-1')) {
        return jsonResponse({ id: 'chan-1', type: 'O' });
      }
      if (url.includes('/api/v4/channels/chan-1/posts?since=')) {
        return jsonResponse({ order: [], posts: {} });
      }
      if (url.endsWith('/api/v4/posts') && init?.method === 'POST') {
        const body = JSON.parse(String(init.body));
        expect(body.channel_id).toBe('chan-1');
        expect(body.message).toBe('hello mattermost');
        return jsonResponse({ id: 'p1' });
      }
      throw new Error(`Unexpected fetch URL: ${url}`);
    });

    const adapter = new MattermostAdapter(baseConfig);
    await adapter.connect();
    await adapter.send('chan-1', { text: 'hello mattermost' });
    await adapter.disconnect();
  });

  it('normalizes inbound message sender/session fields', async () => {
    const adapter = new MattermostAdapter({ ...baseConfig, requireMention: false });
    const messages: InboundMessage[] = [];
    adapter.onMessage((msg) => messages.push(msg));

    (adapter as unknown as { botUserId: string }).botUserId = 'bot-user';
    await (adapter as unknown as {
      processPosts: (channelId: string, response: unknown) => Promise<void>;
    }).processPosts('chan-1', {
      order: ['p1'],
      posts: {
        p1: {
          id: 'p1',
          channel_id: 'chan-1',
          user_id: 'user-123',
          message: 'hello',
          create_at: 1700000000000,
        },
      },
    });

    expect(messages).toHaveLength(1);
    expect(messages[0].channel).toBe('mattermost');
    expect(messages[0].senderId).toBe('chan-1');
    expect(messages[0].metadata).toMatchObject({
      channelId: 'chan-1',
      senderUserId: 'user-123',
    });
  });

  it('uploads binary attachments and posts with file_ids', async () => {
    mockFetch.mockImplementation(async (url: string, init?: RequestInit) => {
      if (url.endsWith('/api/v4/users/me')) {
        return jsonResponse({ id: 'bot-user', username: 'flynnbot' });
      }
      if (url.endsWith('/api/v4/channels/chan-1')) {
        return jsonResponse({ id: 'chan-1', type: 'O' });
      }
      if (url.includes('/api/v4/channels/chan-1/posts?since=')) {
        return jsonResponse({ order: [], posts: {} });
      }
      if (url.endsWith('/api/v4/files') && init?.method === 'POST') {
        return jsonResponse({ file_infos: [{ id: 'file-1' }] });
      }
      if (url.endsWith('/api/v4/posts') && init?.method === 'POST') {
        const body = JSON.parse(String(init.body));
        if (Array.isArray(body.file_ids)) {
          expect(body.channel_id).toBe('chan-1');
          expect(body.file_ids).toEqual(['file-1']);
          return jsonResponse({ id: 'p2' });
        }
        return jsonResponse({ id: 'p1' });
      }
      throw new Error(`Unexpected fetch URL: ${url}`);
    });

    const adapter = new MattermostAdapter(baseConfig);
    await adapter.connect();
    await adapter.send('chan-1', {
      text: '',
      attachments: [{ mimeType: 'image/png', data: 'aGVsbG8=', filename: 'image.png' }],
    });
    await adapter.disconnect();
  });

  it('enforces channel allowlist and mention gating', async () => {
    const adapter = new MattermostAdapter(baseConfig);
    const messages: InboundMessage[] = [];
    adapter.onMessage((msg) => messages.push(msg));

    (adapter as unknown as { botUserId: string }).botUserId = 'bot-user';
    (adapter as unknown as { botUsername: string }).botUsername = 'flynnbot';

    await (adapter as unknown as {
      processPosts: (channelId: string, response: unknown) => Promise<void>;
    }).processPosts('chan-1', {
      order: ['p1', 'p2'],
      posts: {
        p1: {
          id: 'p1',
          channel_id: 'chan-1',
          user_id: 'user-1',
          message: 'hello no mention',
          create_at: 1000,
        },
        p2: {
          id: 'p2',
          channel_id: 'chan-1',
          user_id: 'user-1',
          message: '@flynn hello yes mention',
          create_at: 1001,
        },
      },
    });

    await (adapter as unknown as {
      processPosts: (channelId: string, response: unknown) => Promise<void>;
    }).processPosts('chan-2', {
      order: ['p3'],
      posts: {
        p3: {
          id: 'p3',
          channel_id: 'chan-2',
          user_id: 'user-2',
          message: '@flynn should be dropped by allowlist',
          create_at: 1002,
        },
      },
    });

    expect(messages).toHaveLength(1);
    expect(messages[0].text).toBe('hello yes mention');
  });

  it('sets error status on connect failure and supports reconnect', async () => {
    mockFetch
      .mockResolvedValueOnce(jsonResponse({ error: 'bad token' }, 401))
      .mockResolvedValueOnce(jsonResponse({ id: 'bot-user', username: 'flynnbot' }))
      .mockResolvedValueOnce(jsonResponse({ id: 'chan-1', type: 'O' }))
      .mockResolvedValueOnce(jsonResponse({ order: [], posts: {} }));

    const adapter = new MattermostAdapter(baseConfig);
    await expect(adapter.connect()).rejects.toThrow('Mattermost GET /api/v4/users/me failed');
    expect(adapter.status).toBe('error');

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