import { describe, expect, it, vi, beforeEach } from 'vitest';
import type { IncomingMessage, ServerResponse } from 'http';

import { ZaloAdapter } from './adapter.js';

const mockFetch = vi.fn();
vi.stubGlobal('fetch', mockFetch);

function mockReq(body: string, token?: string): IncomingMessage {
  const req = {
    headers: token ? { 'x-zalo-token': token } : {},
    on(event: string, handler: (...args: unknown[]) => void) {
      if (event === 'data') {
        handler(Buffer.from(body, 'utf8'));
      }
      if (event === 'end') {
        handler();
      }
      return this;
    },
    off: () => req,
    destroy: () => undefined,
  } as unknown as IncomingMessage;
  return req;
}

function mockRes() {
  const state = { statusCode: 0, body: '' };
  const res = {
    writeHead: (code: number) => {
      state.statusCode = code;
    },
    end: (chunk?: string) => {
      state.body = chunk ?? '';
    },
  } as unknown as ServerResponse;
  return { res, state };
}

describe('ZaloAdapter', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockFetch.mockReset();
  });

  it('has name zalo and starts disconnected', () => {
    const adapter = new ZaloAdapter({ oaAccessToken: 'token' });
    expect(adapter.name).toBe('zalo');
    expect(adapter.status).toBe('disconnected');
  });

  it('send posts to zalo message API', async () => {
    const adapter = new ZaloAdapter({ oaAccessToken: 'token' });
    await adapter.connect();
    mockFetch.mockResolvedValue({
      ok: true,
      status: 200,
      text: async () => '',
    } as Response);

    await adapter.send('uid-1', { text: 'hello zalo' });
    expect(mockFetch).toHaveBeenCalledTimes(1);
    expect(String(mockFetch.mock.calls[0]?.[0])).toContain('/v3.0/oa/message/cs');
  });

  it('send emits URL attachments and warns for binary attachments', async () => {
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
    const adapter = new ZaloAdapter({ oaAccessToken: 'token' });
    await adapter.connect();
    mockFetch.mockResolvedValue({
      ok: true,
      status: 200,
      text: async () => '',
    } as Response);

    await adapter.send('uid-1', {
      text: 'hello zalo',
      attachments: [
        { mimeType: 'text/plain', url: 'https://example.com/file.txt', filename: 'file.txt' },
        { mimeType: 'application/pdf', data: 'aGVsbG8=' },
      ],
    });

    expect(mockFetch).toHaveBeenCalledTimes(3);
    const secondBody = JSON.parse(String(mockFetch.mock.calls[1]?.[1]?.body ?? '{}'));
    const thirdBody = JSON.parse(String(mockFetch.mock.calls[2]?.[1]?.body ?? '{}'));
    expect(secondBody.message?.text).toBe('file.txt: https://example.com/file.txt');
    expect(thirdBody.message?.text).toBe('[Zalo] Binary attachment not uploaded yet: attachment (application/pdf).');
    expect(warnSpy).toHaveBeenCalledWith('Zalo: skipping attachment data (application/pdf) — upload not implemented');
    warnSpy.mockRestore();
  });

  it('uploads binary attachments to MinIO and sends share URL when configured', async () => {
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
    const adapter = new ZaloAdapter({
      oaAccessToken: 'token',
      minio: {
        enabled: true,
        endpoint: 'localhost:9000',
        accessKey: 'minio',
        secretKey: 'secret',
        bucket: 'flynn',
        prefix: 'channels/zalo',
        secure: false,
      },
      minioExecRunner: vi.fn(async (_file, args) => {
        if (args[0] === 'share') {
          return { stdout: '{"share":"https://minio.local/share/file.pdf"}\n', stderr: '' };
        }
        return { stdout: '', stderr: '' };
      }),
    });
    await adapter.connect();
    mockFetch.mockResolvedValue({
      ok: true,
      status: 200,
      text: async () => '',
    } as Response);

    await adapter.send('uid-1', {
      text: '',
      attachments: [
        { mimeType: 'application/pdf', data: 'aGVsbG8=', filename: 'file.pdf' },
      ],
    });

    expect(mockFetch).toHaveBeenCalledTimes(1);
    const body = JSON.parse(String(mockFetch.mock.calls[0]?.[1]?.body ?? '{}'));
    expect(body.message?.text).toBe('file.pdf: https://minio.local/share/file.pdf');
    expect(warnSpy).not.toHaveBeenCalled();
    warnSpy.mockRestore();
  });

  it('falls back to warning notice when MinIO upload fails', async () => {
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
    const adapter = new ZaloAdapter({
      oaAccessToken: 'token',
      minio: {
        enabled: true,
        endpoint: 'localhost:9000',
        accessKey: 'minio',
        secretKey: 'secret',
        bucket: 'flynn',
        prefix: 'channels/zalo',
        secure: false,
      },
      minioExecRunner: vi.fn(async () => {
        throw new Error('mc unavailable');
      }),
    });
    await adapter.connect();
    mockFetch.mockResolvedValue({
      ok: true,
      status: 200,
      text: async () => '',
    } as Response);

    await adapter.send('uid-1', {
      text: '',
      attachments: [{ mimeType: 'application/pdf', data: 'aGVsbG8=', filename: 'file.pdf' }],
    });

    expect(mockFetch).toHaveBeenCalledTimes(1);
    const body = JSON.parse(String(mockFetch.mock.calls[0]?.[1]?.body ?? '{}'));
    expect(body.message?.text).toBe('[Zalo] Binary attachment not uploaded yet: file.pdf (application/pdf).');
    expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Zalo: MinIO upload failed'));
    expect(warnSpy).toHaveBeenCalledWith('Zalo: skipping attachment data (application/pdf) — upload not implemented');
    warnSpy.mockRestore();
  });

  it('send delivers URL attachment even when text is empty', async () => {
    const adapter = new ZaloAdapter({ oaAccessToken: 'token' });
    await adapter.connect();
    mockFetch.mockResolvedValue({
      ok: true,
      status: 200,
      text: async () => '',
    } as Response);

    await adapter.send('uid-1', {
      text: ' ',
      attachments: [{ mimeType: 'text/plain', url: 'https://example.com/file.txt', filename: 'file.txt' }],
    });

    expect(mockFetch).toHaveBeenCalledTimes(1);
    const body = JSON.parse(String(mockFetch.mock.calls[0]?.[1]?.body ?? '{}'));
    expect(body.message?.text).toBe('file.txt: https://example.com/file.txt');
  });

  it('handleEvent emits inbound message', async () => {
    const adapter = new ZaloAdapter({ oaAccessToken: 'token', requireMention: false });
    const inbound: Array<{ channel: string; senderId: string; text: string }> = [];
    adapter.onMessage((msg) => inbound.push({ channel: msg.channel, senderId: msg.senderId, text: msg.text }));

    await adapter.handleEvent({
      sender: { id: 'uid-1' },
      recipient: { id: 'oa-1' },
      message: { msg_id: 'm1', text: 'ping' },
      timestamp: 123,
    });

    expect(inbound).toEqual([{ channel: 'zalo', senderId: 'uid-1', text: 'ping' }]);
  });

  it('enforces webhook token when configured', async () => {
    const adapter = new ZaloAdapter({ oaAccessToken: 'token', webhookToken: 'secret' });
    const body = JSON.stringify({
      sender: { id: 'uid-1' },
      message: { msg_id: 'm1', text: 'ping' },
    });
    const req = mockReq(body, 'wrong');
    const { res, state } = mockRes();

    await adapter.handleRequest(req, res);
    expect(state.statusCode).toBe(401);
  });

  it('drops messages missing required mention', async () => {
    const adapter = new ZaloAdapter({ oaAccessToken: 'token', requireMention: true, mentionName: 'flynn' });
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.handleEvent({
      sender: { id: 'uid-1' },
      message: { msg_id: 'm1', text: 'hello there' },
    });

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