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

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

vi.mock('googleapis', () => ({
  google: {
    auth: {
      OAuth2: vi.fn().mockImplementation(() => ({
        setCredentials: vi.fn(),
      })),
    },
    calendar: vi.fn().mockReturnValue({
      events: {
        list: mockEventsList,
      },
    }),
  },
}));

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

import { createGcalTools } from './gcal.js';

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

const testConfig: NonNullable<GcalConfig> = {
  enabled: true,
  credentials_file: '/tmp/test-creds.json',
  token_file: '/tmp/test-token.json',
  calendar_ids: ['primary'],
};

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 mockCalendarEvent(
  id: string,
  summary: string,
  startTime: string,
  endTime: string,
  opts?: { location?: string; attendees?: string[]; htmlLink?: string },
) {
  return {
    id,
    summary,
    start: { dateTime: startTime },
    end: { dateTime: endTime },
    location: opts?.location,
    attendees: opts?.attendees?.map(email => ({ email })),
    htmlLink: opts?.htmlLink ?? `https://calendar.google.com/event/${id}`,
  };
}

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

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

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

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

describe('calendar.today', () => {
  it('returns error when credentials file missing', async () => {
    mockExistsSync.mockReturnValue(false);
    const [todayTool] = createGcalTools(testConfig);

    const result = await todayTool.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 [todayTool] = createGcalTools(testConfig);

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

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

  it('lists today\'s events', async () => {
    setupValidAuth();
    mockEventsList.mockResolvedValue({
      data: {
        items: [
          mockCalendarEvent('ev1', 'Team standup', '2026-02-10T09:00:00Z', '2026-02-10T09:30:00Z', {
            location: 'Room A',
            attendees: ['alice@test.com', 'bob@test.com'],
          }),
          mockCalendarEvent('ev2', 'Lunch', '2026-02-10T12:00:00Z', '2026-02-10T13:00:00Z'),
        ],
      },
    });

    const [todayTool] = createGcalTools(testConfig);
    const result = await todayTool.execute({});

    expect(result.success).toBe(true);
    expect(result.output).toContain('Team standup');
    expect(result.output).toContain('Room A');
    expect(result.output).toContain('alice@test.com');
    expect(result.output).toContain('Lunch');

    expect(mockEventsList).toHaveBeenCalledWith(
      expect.objectContaining({
        calendarId: 'primary',
        singleEvents: true,
        orderBy: 'startTime',
      }),
    );
  });

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

    const [todayTool] = createGcalTools(testConfig);
    const result = await todayTool.execute({});

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

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

    const [todayTool] = createGcalTools(testConfig);
    const result = await todayTool.execute({});

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

  it('respects calendarId parameter', async () => {
    setupValidAuth();
    mockEventsList.mockResolvedValue({ data: { items: [] } });

    const [todayTool] = createGcalTools(testConfig);
    await todayTool.execute({ calendarId: 'work@group.calendar.google.com' });

    expect(mockEventsList).toHaveBeenCalledWith(
      expect.objectContaining({
        calendarId: 'work@group.calendar.google.com',
      }),
    );
  });
});

describe('calendar.list', () => {
  it('queries events in date range', async () => {
    setupValidAuth();
    mockEventsList.mockResolvedValue({
      data: {
        items: [
          mockCalendarEvent('ev1', 'Conference', '2026-02-15T10:00:00Z', '2026-02-15T17:00:00Z'),
        ],
      },
    });

    const [, listTool] = createGcalTools(testConfig);
    const result = await listTool.execute({ startDate: '2026-02-15', endDate: '2026-02-16' });

    expect(result.success).toBe(true);
    expect(result.output).toContain('Conference');

    expect(mockEventsList).toHaveBeenCalledWith(
      expect.objectContaining({
        calendarId: 'primary',
        singleEvents: true,
        orderBy: 'startTime',
      }),
    );
  });

  it('returns error for invalid dates', async () => {
    setupValidAuth();
    const [, listTool] = createGcalTools(testConfig);
    const result = await listTool.execute({ startDate: 'not-a-date', endDate: '2026-02-10' });

    expect(result.success).toBe(false);
    expect(result.error).toContain('Invalid date format');
  });

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

    const [, listTool] = createGcalTools(testConfig);
    await listTool.execute({ startDate: '2026-02-10', endDate: '2026-02-11', maxResults: 5 });

    expect(mockEventsList).toHaveBeenCalledWith(
      expect.objectContaining({
        maxResults: 5,
      }),
    );
  });

  it('respects calendarId param', async () => {
    setupValidAuth();
    mockEventsList.mockResolvedValue({ data: { items: [] } });

    const [, listTool] = createGcalTools(testConfig);
    await listTool.execute({ startDate: '2026-02-10', endDate: '2026-02-11', calendarId: 'work@group.calendar.google.com' });

    expect(mockEventsList).toHaveBeenCalledWith(
      expect.objectContaining({
        calendarId: 'work@group.calendar.google.com',
      }),
    );
  });
});

describe('calendar.search', () => {
  it('searches with query parameter', async () => {
    setupValidAuth();
    mockEventsList.mockResolvedValue({
      data: {
        items: [
          mockCalendarEvent('ev1', 'Team planning', '2026-02-10T14:00:00Z', '2026-02-10T15:00:00Z'),
        ],
      },
    });

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

    expect(result.success).toBe(true);
    expect(result.output).toContain('Team planning');

    expect(mockEventsList).toHaveBeenCalledWith(
      expect.objectContaining({
        q: 'planning',
        singleEvents: true,
        orderBy: 'startTime',
      }),
    );
  });

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

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

    expect(mockEventsList).toHaveBeenCalledWith(
      expect.objectContaining({
        q: 'meeting',
        maxResults: 3,
      }),
    );
  });

  it('returns error when credentials missing', async () => {
    mockExistsSync.mockReturnValue(false);
    const [, , searchTool] = createGcalTools(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();
    mockEventsList.mockRejectedValue(new Error('API quota exceeded'));

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

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

  it('formats events with all fields', async () => {
    setupValidAuth();
    mockEventsList.mockResolvedValue({
      data: {
        items: [
          mockCalendarEvent('ev1', 'Board meeting', '2026-03-01T10:00:00Z', '2026-03-01T12:00:00Z', {
            location: 'HQ Conference Room',
            attendees: ['ceo@company.com', 'cfo@company.com'],
            htmlLink: 'https://calendar.google.com/event/ev1',
          }),
        ],
      },
    });

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

    expect(result.success).toBe(true);
    expect(result.output).toContain('Board meeting');
    expect(result.output).toContain('HQ Conference Room');
    expect(result.output).toContain('ceo@company.com');
    expect(result.output).toContain('cfo@company.com');
    expect(result.output).toContain('https://calendar.google.com/event/ev1');
  });
});
