import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { GmailConfig } from '../../config/schema.js';

// Hoisted mocks so vi.mock factories can reference them
const { mockMessagesList, mockMessagesGet, mockExistsSync, mockReadFileSync } = vi.hoisted(() => ({
  mockMessagesList: vi.fn(),
  mockMessagesGet: vi.fn(),
  mockExistsSync: vi.fn(),
  mockReadFileSync: vi.fn(),
}));

vi.mock('googleapis', () => ({
  google: {
    auth: {
      OAuth2: vi.fn().mockImplementation(() => ({
        setCredentials: vi.fn(),
      })),
    },
    gmail: vi.fn().mockReturnValue({
      users: {
        messages: {
          list: mockMessagesList,
          get: mockMessagesGet,
        },
      },
    }),
  },
}));

vi.mock('fs', async () => {
  const actual = await vi.importActual<typeof import('fs')>('fs');
  return {
    ...actual,
    existsSync: mockExistsSync,
    readFileSync: mockReadFileSync,
  };
});

import { createGmailTools } from './gmail.js';

// ── Test config ─────────────────────────────────────────────────────────────

const testConfig: NonNullable<GmailConfig> = {
  enabled: true,
  credentials_file: '/tmp/test-creds.json',
  token_file: '/tmp/test-token.json',
  disable_push: false,
  pubsub_pull_interval: '60s',
  pubsub_max_messages: 10,
  watch_labels: ['INBOX'],
  poll_interval: '300s',
  output: { channel: 'discord', peer: '123' },
  message: '{{from}}: {{subject}}',
};

const fakeCredentials = {
  installed: {
    client_id: 'test-client-id',
    client_secret: 'test-client-secret',
    redirect_uris: ['http://localhost'],
  },
};

const fakeToken = {
  access_token: 'test-access-token',
  refresh_token: 'test-refresh-token',
};

// ── Helpers ─────────────────────────────────────────────────────────────────

function setupValidAuth() {
  mockExistsSync.mockReturnValue(true);
  mockReadFileSync.mockImplementation((path: unknown) => {
    const p = String(path);
    if (p.includes('creds')) {return JSON.stringify(fakeCredentials);}
    if (p.includes('token')) {return JSON.stringify(fakeToken);}
    return '';
  });
}

function mockMessageDetails(id: string, from: string, subject: string, date: string, snippet: string) {
  return {
    data: {
      payload: {
        headers: [
          { name: 'From', value: from },
          { name: 'Subject', value: subject },
          { name: 'Date', value: date },
        ],
      },
      snippet,
    },
  };
}

// ═════════════════════════════════════════════════════════════════════════════

beforeEach(() => {
  vi.clearAllMocks();
});

describe('createGmailTools', () => {
  it('returns 3 tools with correct names', () => {
    const tools = createGmailTools(testConfig);
    expect(tools).toHaveLength(3);
    expect(tools.map(t => t.name)).toEqual(['gmail.list', 'gmail.search', 'gmail.read']);
  });

  it('tools have descriptions and input schemas', () => {
    const tools = createGmailTools(testConfig);
    for (const tool of tools) {
      expect(tool.description).toBeTruthy();
      expect(tool.inputSchema).toBeDefined();
      expect(tool.inputSchema.type).toBe('object');
    }
  });
});

describe('gmail.list', () => {
  it('returns error when credentials file missing', async () => {
    mockExistsSync.mockReturnValue(false);
    const [listTool] = createGmailTools(testConfig);

    const result = await listTool.execute({});

    expect(result.success).toBe(false);
    expect(result.error).toContain('Credentials file not found');
  });

  it('returns error when token file missing', async () => {
    mockExistsSync.mockImplementation((path: unknown) => {
      return String(path).includes('creds');
    });
    mockReadFileSync.mockReturnValue(JSON.stringify(fakeCredentials));
    const [listTool] = createGmailTools(testConfig);

    const result = await listTool.execute({});

    expect(result.success).toBe(false);
    expect(result.error).toContain('Token file not found');
  });

  it('lists recent emails with default params', async () => {
    setupValidAuth();
    mockMessagesList.mockResolvedValue({
      data: {
        messages: [{ id: 'msg1' }, { id: 'msg2' }],
      },
    });
    mockMessagesGet
      .mockResolvedValueOnce(mockMessageDetails('msg1', 'alice@test.com', 'Hello', 'Mon, 10 Feb 2026', 'Hi there'))
      .mockResolvedValueOnce(mockMessageDetails('msg2', 'bob@test.com', 'Meeting', 'Mon, 10 Feb 2026', 'At 3pm'));

    const [listTool] = createGmailTools(testConfig);
    const result = await listTool.execute({});

    expect(result.success).toBe(true);
    expect(result.output).toContain('alice@test.com');
    expect(result.output).toContain('Hello');
    expect(result.output).toContain('bob@test.com');
    expect(result.output).toContain('Meeting');

    expect(mockMessagesList).toHaveBeenCalledWith(
      expect.objectContaining({
        userId: 'me',
        labelIds: ['INBOX'],
        maxResults: 10,
      }),
    );
  });

  it('respects maxResults and label params', async () => {
    setupValidAuth();
    mockMessagesList.mockResolvedValue({ data: { messages: [] } });

    const [listTool] = createGmailTools(testConfig);
    await listTool.execute({ maxResults: 5, label: 'SENT' });

    expect(mockMessagesList).toHaveBeenCalledWith(
      expect.objectContaining({
        labelIds: ['SENT'],
        maxResults: 5,
      }),
    );
  });

  it('handles empty results', async () => {
    setupValidAuth();
    mockMessagesList.mockResolvedValue({ data: { messages: [] } });

    const [listTool] = createGmailTools(testConfig);
    const result = await listTool.execute({});

    expect(result.success).toBe(true);
    expect(result.output).toBe('No messages found.');
  });

  it('sanitizes HTML entities in snippets', async () => {
    setupValidAuth();
    mockMessagesList.mockResolvedValue({
      data: {
        messages: [{ id: 'msg1' }],
      },
    });
    mockMessagesGet.mockResolvedValueOnce(
      mockMessageDetails(
        'msg1',
        'experian@test.com',
        'Credit Alert',
        'Mon, 10 Feb 2026',
        'William, your score is rising&#39;s &amp; it&#8230; Don&apos;t miss out<br>Check now',
      ),
    );

    const [listTool] = createGmailTools(testConfig);
    const result = await listTool.execute({});

    expect(result.success).toBe(true);
    expect(result.output).not.toContain('&#39;');
    expect(result.output).not.toContain('&amp;');
    expect(result.output).not.toContain('&#8230;');
    expect(result.output).not.toContain('<br>');
    expect(result.output).toContain("rising's");
    expect(result.output).toContain('& it');
    expect(result.output).toContain("Don't miss out");
  });
});

describe('gmail.search', () => {
  it('searches with query parameter', async () => {
    setupValidAuth();
    mockMessagesList.mockResolvedValue({
      data: {
        messages: [{ id: 'msg1' }],
      },
    });
    mockMessagesGet.mockResolvedValueOnce(
      mockMessageDetails('msg1', 'alice@test.com', 'Invoice', 'Mon, 10 Feb 2026', 'Your invoice'),
    );

    const [, searchTool] = createGmailTools(testConfig);
    const result = await searchTool.execute({ query: 'from:alice subject:invoice' });

    expect(result.success).toBe(true);
    expect(result.output).toContain('Invoice');
    expect(result.output).toContain('alice@test.com');

    expect(mockMessagesList).toHaveBeenCalledWith(
      expect.objectContaining({
        userId: 'me',
        q: 'from:alice subject:invoice',
        maxResults: 10,
      }),
    );
  });

  it('respects maxResults param', async () => {
    setupValidAuth();
    mockMessagesList.mockResolvedValue({ data: { messages: [] } });

    const [, searchTool] = createGmailTools(testConfig);
    await searchTool.execute({ query: 'is:unread', maxResults: 3 });

    expect(mockMessagesList).toHaveBeenCalledWith(
      expect.objectContaining({
        q: 'is:unread',
        maxResults: 3,
      }),
    );
  });

  it('returns error when credentials missing', async () => {
    mockExistsSync.mockReturnValue(false);
    const [, searchTool] = createGmailTools(testConfig);

    const result = await searchTool.execute({ query: 'test' });

    expect(result.success).toBe(false);
    expect(result.error).toContain('Credentials file not found');
  });

  it('handles API errors gracefully', async () => {
    setupValidAuth();
    mockMessagesList.mockRejectedValue(new Error('API quota exceeded'));

    const [, searchTool] = createGmailTools(testConfig);
    const result = await searchTool.execute({ query: 'test' });

    expect(result.success).toBe(false);
    expect(result.error).toContain('API quota exceeded');
  });
});

// ── gmail.read ──────────────────────────────────────────────────────────────

/** Encode a string as base64url (matching Gmail API format). */
function toBase64Url(str: string): string {
  return Buffer.from(str).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

describe('gmail.read', () => {
  it('reads full message with plain text body', async () => {
    setupValidAuth();
    const bodyText = 'Hello, this is the full email body with invoice amount $150.00';
    mockMessagesGet.mockResolvedValue({
      data: {
        payload: {
          mimeType: 'text/plain',
          headers: [
            { name: 'From', value: 'billing@example.com' },
            { name: 'To', value: 'will@example.com' },
            { name: 'Subject', value: 'Payment Receipt' },
            { name: 'Date', value: 'Mon, 10 Feb 2026 12:00:00 -0000' },
          ],
          body: { data: toBase64Url(bodyText) },
        },
      },
    });

    const [, , readTool] = createGmailTools(testConfig);
    const result = await readTool.execute({ id: 'msg123' });

    expect(result.success).toBe(true);
    expect(result.output).toContain('From: billing@example.com');
    expect(result.output).toContain('To: will@example.com');
    expect(result.output).toContain('Subject: Payment Receipt');
    expect(result.output).toContain('$150.00');

    expect(mockMessagesGet).toHaveBeenCalledWith(
      expect.objectContaining({
        userId: 'me',
        id: 'msg123',
        format: 'full',
      }),
    );
  });

  it('reads multipart message preferring text/plain', async () => {
    setupValidAuth();
    const plainBody = 'Plain text version of the email';
    const htmlBody = '<html><body><b>HTML version</b></body></html>';
    mockMessagesGet.mockResolvedValue({
      data: {
        payload: {
          mimeType: 'multipart/alternative',
          headers: [
            { name: 'From', value: 'sender@example.com' },
            { name: 'To', value: 'will@example.com' },
            { name: 'Subject', value: 'Multipart Test' },
            { name: 'Date', value: 'Mon, 10 Feb 2026 12:00:00 -0000' },
          ],
          parts: [
            { mimeType: 'text/html', body: { data: toBase64Url(htmlBody) } },
            { mimeType: 'text/plain', body: { data: toBase64Url(plainBody) } },
          ],
        },
      },
    });

    const [, , readTool] = createGmailTools(testConfig);
    const result = await readTool.execute({ id: 'msg456' });

    expect(result.success).toBe(true);
    expect(result.output).toContain('Plain text version of the email');
    expect(result.output).not.toContain('<html>');
  });

  it('falls back to stripped HTML when no text/plain part', async () => {
    setupValidAuth();
    const htmlBody = '<html><body><p>Amount: $200.00</p></body></html>';
    mockMessagesGet.mockResolvedValue({
      data: {
        payload: {
          mimeType: 'multipart/alternative',
          headers: [
            { name: 'From', value: 'sender@example.com' },
            { name: 'To', value: 'will@example.com' },
            { name: 'Subject', value: 'HTML Only' },
            { name: 'Date', value: 'Mon, 10 Feb 2026 12:00:00 -0000' },
          ],
          parts: [
            { mimeType: 'text/html', body: { data: toBase64Url(htmlBody) } },
          ],
        },
      },
    });

    const [, , readTool] = createGmailTools(testConfig);
    const result = await readTool.execute({ id: 'msg789' });

    expect(result.success).toBe(true);
    expect(result.output).toContain('Amount: $200.00');
    expect(result.output).not.toContain('<html>');
  });

  it('decodes HTML entities in HTML-only body fallback', async () => {
    setupValidAuth();
    const htmlBody = '<html><body><p>Hello &amp; welcome</p><br><p>Price: &lt;$100&gt;</p><br><p>It&#39;s great</p></body></html>';
    mockMessagesGet.mockResolvedValue({
      data: {
        payload: {
          mimeType: 'multipart/alternative',
          headers: [
            { name: 'From', value: 'sender@example.com' },
            { name: 'To', value: 'will@example.com' },
            { name: 'Subject', value: 'HTML Entities' },
            { name: 'Date', value: 'Mon, 10 Feb 2026 12:00:00 -0000' },
          ],
          parts: [
            { mimeType: 'text/html', body: { data: toBase64Url(htmlBody) } },
          ],
        },
      },
    });

    const [, , readTool] = createGmailTools(testConfig);
    const result = await readTool.execute({ id: 'msg-entities' });

    expect(result.success).toBe(true);
    expect(result.output).toContain('Hello & welcome');
    expect(result.output).toContain('Price: <$100>');
    expect(result.output).toContain("It's great");
    expect(result.output).not.toContain('&amp;');
    expect(result.output).not.toContain('&lt;');
    expect(result.output).not.toContain('&#39;');
  });

  it('returns error when credentials missing', async () => {
    mockExistsSync.mockReturnValue(false);
    const [, , readTool] = createGmailTools(testConfig);

    const result = await readTool.execute({ id: 'msg1' });

    expect(result.success).toBe(false);
    expect(result.error).toContain('Credentials file not found');
  });

  it('handles API errors gracefully', async () => {
    setupValidAuth();
    mockMessagesGet.mockRejectedValue(new Error('Message not found'));

    const [, , readTool] = createGmailTools(testConfig);
    const result = await readTool.execute({ id: 'nonexistent' });

    expect(result.success).toBe(false);
    expect(result.error).toContain('Message not found');
  });
});
