import { beforeEach, describe, expect, it, vi } from 'vitest'
import { Hono } from 'hono'
import type { AppEnv } from '../src/types'

const mocks = vi.hoisted(() => ({
  requirePermission: vi.fn(() => async (_c: unknown, next: () => Promise<void>) => {
    await next()
  }),
  listCalendarConnections: vi.fn(),
  getCalendarConnection: vi.fn(),
  deleteCalendarConnection: vi.fn(),
  updateCalendarConnection: vi.fn(),
  listCalendarSettingsConnections: vi.fn(),
  updateCalendarConnectionPrefs: vi.fn(),
  createDb: vi.fn(() => ({ mock: true })),
  listCalendars: vi.fn(),
  registerWatch: vi.fn(),
}))

vi.mock('../src/middleware/guards', () => ({
  requirePermission: mocks.requirePermission,
}))

vi.mock('../src/middleware/auth', () => ({
  authMiddleware: async (c: AppEnv['Variables']['c'], next: () => Promise<void>) => {
    await next()
  },
}))

vi.mock('@zync/db/queries', async (importOriginal) => {
  const actual = await importOriginal<typeof import('@zync/db/queries')>()
  return {
    ...actual,
    listCalendarConnections: mocks.listCalendarConnections,
    getCalendarConnection: mocks.getCalendarConnection,
    deleteCalendarConnection: mocks.deleteCalendarConnection,
    updateCalendarConnection: mocks.updateCalendarConnection,
    listCalendarSettingsConnections: mocks.listCalendarSettingsConnections,
    updateCalendarConnectionPrefs: mocks.updateCalendarConnectionPrefs,
    createDb: mocks.createDb,
  }
})

vi.mock('@zync/calendar', async (importOriginal) => {
  const actual = await importOriginal<typeof import('@zync/calendar')>()
  return {
    ...actual,
    GoogleCalendarProvider: vi.fn().mockImplementation(() => ({
      listCalendars: mocks.listCalendars,
      registerWatch: mocks.registerWatch,
    })),
    OutlookCalendarProvider: vi.fn().mockImplementation(() => ({
      listCalendars: mocks.listCalendars,
      registerWatch: mocks.registerWatch,
    })),
  }
})

vi.mock('@zync/calendar/server', () => ({
  decryptToken: vi.fn(async () => 'access-token'),
  encryptToken: vi.fn(async () => new Uint8Array([1, 2, 3])),
}))

import { calendarConnectionsRoute } from '../src/routes/calendar/connections'
import { calendarSettingsRoute } from '../src/routes/settings/calendar'

function appWithRoute(route: Hono<AppEnv>) {
  const app = new Hono<AppEnv>()
  app.use('*', async (c, next) => {
    c.set('session', {
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      roles: [],
      permissions: [],
    })
    c.set('db', { mock: true })
    await next()
  })
  app.route('/', route)
  return app
}

describe('calendar connection routes', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mocks.listCalendars.mockResolvedValue([
      { id: 'primary', name: 'Primary', isPrimary: true },
      { id: 'team', name: 'Team', isPrimary: false },
    ])
    mocks.registerWatch.mockResolvedValue({ channelId: 'channel-1' })
    mocks.getCalendarConnection.mockResolvedValue({
      id: 'conn-1',
      tenantId: 'tenant-1',
      userId: 'user-1',
      provider: 'google',
      externalUserId: 'ext-1',
      accessToken: new Uint8Array([1]),
      refreshToken: new Uint8Array([2]),
      selectedCalendarId: null,
      syncEnabled: true,
      tokenExpiresAt: null,
      lastSyncedAt: null,
      createdAt: new Date('2026-01-01T00:00:00Z'),
    })
    mocks.updateCalendarConnection.mockResolvedValue({
      id: 'conn-1',
      tenantId: 'tenant-1',
      userId: 'user-1',
      provider: 'google',
      externalUserId: 'ext-1',
      selectedCalendarId: 'primary',
      syncEnabled: true,
      lastSyncedAt: null,
      createdAt: new Date('2026-01-01T00:00:00Z'),
    })
  })

  it('lists provider calendars for the owning connection', async () => {
    const res = await appWithRoute(calendarConnectionsRoute).request(
      '/connections/conn-1/calendars',
      { method: 'GET' },
      {
        INTEGRATION_ENCRYPTION_KEY: 'x'.repeat(32),
        GOOGLE_OAUTH_CLIENT_ID: 'google-client',
        GOOGLE_OAUTH_CLIENT_SECRET: 'google-secret',
      } as AppEnv['Bindings'],
    )

    expect(res.status).toBe(200)
    expect(await res.json()).toEqual({
      calendars: [
        { id: 'primary', name: 'Primary', isPrimary: true },
        { id: 'team', name: 'Team', isPrimary: false },
      ],
    })
  })

  it('updates selected calendar and sync flag for the owning connection', async () => {
    const res = await appWithRoute(calendarConnectionsRoute).request(
      '/connections/conn-1',
      {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ selectedCalendarId: 'primary', syncEnabled: true }),
      },
      {
        INTEGRATION_ENCRYPTION_KEY: 'x'.repeat(32),
        GOOGLE_OAUTH_CLIENT_ID: 'google-client',
        GOOGLE_OAUTH_CLIENT_SECRET: 'google-secret',
        KV: {
          put: vi.fn(async () => undefined),
        },
      } as unknown as AppEnv['Bindings'],
    )

    expect(res.status).toBe(200)
    expect(mocks.updateCalendarConnection).toHaveBeenCalledWith(
      { mock: true },
      'tenant-1',
      'conn-1',
      expect.objectContaining({ selectedCalendarId: 'primary', syncEnabled: true }),
    )
    expect(mocks.registerWatch).toHaveBeenCalled()
  })
})

describe('calendar settings route', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mocks.listCalendarSettingsConnections.mockResolvedValue([
      {
        provider: 'google',
        status: 'active',
        connected_email: 'alex@gmail.com',
        connected_account_label: 'alex@gmail.com',
        selected_calendar_id: 'primary',
        selected_calendar_name: 'alex@gmail.com',
        sync_direction: 'two_way',
        sync_task_due_dates: true,
        sync_manual_events: true,
        sync_customer_meetings: false,
        last_synced_at: null,
        last_sync_error: null,
      },
    ])
    mocks.listCalendarConnections.mockResolvedValue([
      {
        id: 'conn-1',
        tenantId: 'tenant-1',
        userId: 'user-1',
        provider: 'google',
        externalUserId: 'ext-1',
        accessToken: new Uint8Array([1]),
        refreshToken: new Uint8Array([2]),
        tokenExpiresAt: null,
        connectedEmail: 'alex@gmail.com',
        selectedCalendarId: 'primary',
        selectedCalendarName: 'alex@gmail.com',
        syncDirection: 'two_way',
        syncTaskDueDates: true,
        syncManualEvents: true,
        syncCustomerMeetings: false,
        syncEnabled: true,
        status: 'active',
        lastSyncError: null,
        lastSyncedAt: null,
        createdAt: new Date('2026-01-01T00:00:00Z'),
      },
    ])
    mocks.updateCalendarConnectionPrefs.mockResolvedValue({
      provider: 'google',
      status: 'active',
      connected_email: 'alex@gmail.com',
      connected_account_label: 'alex@gmail.com',
      selected_calendar_id: 'team',
      selected_calendar_name: 'Team',
      sync_direction: 'read_only',
      sync_task_due_dates: true,
      sync_manual_events: false,
      sync_customer_meetings: false,
      last_synced_at: null,
      last_sync_error: null,
    })
  })

  it('lists current-user calendar settings connections', async () => {
    const res = await appWithRoute(calendarSettingsRoute).request('/connections', { method: 'GET' })

    expect(res.status).toBe(200)
    expect(mocks.listCalendarSettingsConnections).toHaveBeenCalledWith({ mock: true }, 'tenant-1', 'user-1')
  })

  it('lists available calendars for a provider', async () => {
    const res = await appWithRoute(calendarSettingsRoute).request(
      '/calendars?provider=google',
      { method: 'GET' },
      {
        INTEGRATION_ENCRYPTION_KEY: 'x'.repeat(32),
        GOOGLE_OAUTH_CLIENT_ID: 'google-client',
        GOOGLE_OAUTH_CLIENT_SECRET: 'google-secret',
      } as AppEnv['Bindings'],
    )

    expect(res.status).toBe(200)
    expect(await res.json()).toEqual([
      { id: 'primary', name: 'Primary', primary: true },
      { id: 'team', name: 'Team', primary: false },
    ])
  })

  it('updates provider preferences with spec fields', async () => {
    const res = await appWithRoute(calendarSettingsRoute).request('/connections/google', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        selected_calendar_id: 'team',
        selected_calendar_name: 'Team',
        sync_direction: 'read_only',
        sync_task_due_dates: true,
        sync_manual_events: false,
        sync_customer_meetings: false,
      }),
    })

    expect(res.status).toBe(200)
    expect(mocks.updateCalendarConnectionPrefs).toHaveBeenCalledWith(
      { mock: true },
      'tenant-1',
      'user-1',
      'google',
      expect.objectContaining({
        selected_calendar_id: 'team',
        sync_direction: 'read_only',
      }),
    )
  })

  it('deletes by provider and is idempotent', async () => {
    const res = await appWithRoute(calendarSettingsRoute).request(
      '/connections/google',
      { method: 'DELETE' },
      {} as AppEnv['Bindings'],
    )

    expect(res.status).toBe(204)
    expect(mocks.deleteCalendarConnection).toHaveBeenCalledWith({ mock: true }, 'tenant-1', 'conn-1')
  })
})
