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

let mockSession: SessionPayload | undefined
const mockCreateCheckoutSession = vi.fn()
const mockGetInvoiceHistory = vi.fn()
const mockGetBillingPortalUrl = vi.fn()
const mockCancelSubscription = vi.fn()
const mockMarkSubscriptionCanceled = vi.fn()
const mockReactivateCanceledSubscription = vi.fn()
const mockHandleWebhook = vi.fn()
const mockGetSubscriptionByTenantId = vi.fn()
const mockGetCounterValue = vi.fn()
const mockCountActiveMembers = vi.fn()
const mockGetEnabledModuleIds = vi.fn()
const mockAckTrialExpiry = vi.fn()
const mockCreateNotification = vi.fn()
const mockGetUserEmailAndLocale = vi.fn()
const mockGetTenantById = vi.fn()
const mockSendEmail = vi.fn()

vi.mock('../src/middleware/auth', () => ({
  authMiddleware: async (
    c: { set: (key: string, value: unknown) => void },
    next: () => Promise<void>,
  ) => {
    if (mockSession) c.set('session', mockSession)
    await next()
  },
}))

vi.mock('../src/middleware/block-impersonation-ops', () => ({
  blockDuringImpersonation: () => async (_c: unknown, next: () => Promise<void>) => next(),
}))

vi.mock('@zync/db/queries', async () => {
  const actual = await vi.importActual<typeof import('@zync/db/queries')>('@zync/db/queries')
  return {
    ...actual,
    createDb: vi.fn(() => ({})),
    getSubscriptionByTenantId: (...args: unknown[]) => mockGetSubscriptionByTenantId(...args),
    cancelSubscription: vi.fn(),
    markSubscriptionCanceled: (...args: unknown[]) => mockMarkSubscriptionCanceled(...args),
    reactivateCanceledSubscription: (...args: unknown[]) =>
      mockReactivateCanceledSubscription(...args),
    activateSubscription: vi.fn(),
    getCounterValue: (...args: unknown[]) => mockGetCounterValue(...args),
    countActiveMembers: (...args: unknown[]) => mockCountActiveMembers(...args),
    getEnabledModuleIds: (...args: unknown[]) => mockGetEnabledModuleIds(...args),
    ackTrialExpiry: (...args: unknown[]) => mockAckTrialExpiry(...args),
    createNotification: (...args: unknown[]) => mockCreateNotification(...args),
    getUserEmailAndLocale: (...args: unknown[]) => mockGetUserEmailAndLocale(...args),
    getTenantById: (...args: unknown[]) => mockGetTenantById(...args),
  }
})

vi.mock('@zync/payments', async () => {
  const actual = await vi.importActual<typeof import('@zync/payments')>('@zync/payments')
  return {
    ...actual,
    getPaymentAdapter: vi.fn(() => ({
      createCheckoutSession: mockCreateCheckoutSession,
      getSubscriptionStatus: vi.fn(),
      updateSubscription: vi.fn(),
      cancelSubscription: mockCancelSubscription,
      getInvoiceHistory: mockGetInvoiceHistory,
      handleWebhook: mockHandleWebhook,
      getBillingPortalUrl: mockGetBillingPortalUrl,
    })),
    syncTierToTenant: vi.fn(),
  }
})

vi.mock('@zync/notifications', async () => {
  const actual = await vi.importActual<typeof import('@zync/notifications')>('@zync/notifications')
  return {
    ...actual,
    sendEmail: (...args: unknown[]) => mockSendEmail(...args),
  }
})

import { zyncSubscriptionRoute } from '../src/routes/zync-subscription'

const mockEnv = {
  DB: { connectionString: 'postgresql://test:test@localhost/test' },
  JWT_SECRET: 'test-jwt-secret',
  KV: {} as AppEnv['Bindings']['KV'],
} as AppEnv['Bindings']

function appWithRoute() {
  const app = new Hono<AppEnv>()
  app.route('/api/zync-subscription', zyncSubscriptionRoute)
  return app
}

const OWNER_SESSION: SessionPayload = {
  sub: 'owner-user-id' as SessionPayload['sub'],
  tid: 'tenant-id' as SessionPayload['tid'],
  role: 'OWNER',
  permissions: ['settings:write'],
  tier: 'freelancer',
  type: 'user',
  v: 1,
  enforce_2fa: false,
  two_factor_verified: true,
  exp: Math.floor(Date.now() / 1000) + 3600,
  iat: Math.floor(Date.now() / 1000),
}

describe('GET /api/zync-subscription', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockSession = OWNER_SESSION
    mockGetSubscriptionByTenantId.mockResolvedValue({
      id: 'sub-1',
      tenantId: 'tenant-id',
      tier: 'business',
      status: 'active',
      period: 'monthly',
      adapter: 'stripe',
      trialWarningSentAt: new Date('2026-06-28T12:00:00.000Z').toISOString(),
      trialExpiryAckAt: new Date('2026-06-29T12:00:00.000Z').toISOString(),
    })
    mockGetCounterValue.mockResolvedValue(1_024)
    mockCountActiveMembers.mockResolvedValue(3)
    mockGetEnabledModuleIds.mockResolvedValue(['system', 'crm', 'invoices'])
    mockGetBillingPortalUrl.mockResolvedValue('https://billing.example.com/portal')
  })

  it('returns portal and usage data needed by /settings/plan', async () => {
    const res = await appWithRoute().request('/api/zync-subscription', { method: 'GET' }, mockEnv)

    expect(res.status).toBe(200)
    const body = (await res.json()) as {
      billingPortalUrl: string | null
      usage: { teamMembers: { used: number; limit: number }; activeModules: { used: number } }
      storage: { limitBytes: number }
    }

    expect(body.billingPortalUrl).toBe('https://billing.example.com/portal')
    expect(body.usage).toEqual({
      teamMembers: { used: 3, limit: 8 },
      activeModules: { used: 3 },
    })
  })

  it('computes storage quota from the authoritative subscription tier', async () => {
    const res = await appWithRoute().request('/api/zync-subscription', { method: 'GET' }, mockEnv)

    expect(res.status).toBe(200)
    const body = (await res.json()) as { storage: { limitBytes: number } }
    expect(body.storage.limitBytes).toBe(21_474_836_480)
  })

  it('returns the trial warning and acknowledgement timestamps', async () => {
    const res = await appWithRoute().request('/api/zync-subscription', { method: 'GET' }, mockEnv)

    expect(res.status).toBe(200)
    const body = (await res.json()) as {
      trialWarningSentAt: string | null
      trialExpiryAckAt: string | null
    }

    expect(body.trialWarningSentAt).toBe('2026-06-28T12:00:00.000Z')
    expect(body.trialExpiryAckAt).toBe('2026-06-29T12:00:00.000Z')
  })
})

describe('POST /api/zync-subscription/ack-trial-expiry', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockSession = OWNER_SESSION
    mockAckTrialExpiry.mockResolvedValue({
      trialExpiryAckAt: new Date('2026-07-01T09:30:00.000Z'),
    })
  })

  it('acknowledges the trial-expiry interstitial for the current tenant', async () => {
    const res = await appWithRoute().request(
      '/api/zync-subscription/ack-trial-expiry',
      {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({}),
      },
      mockEnv,
    )

    expect(res.status).toBe(200)
    expect(mockAckTrialExpiry).toHaveBeenCalledWith({}, 'tenant-id')
    await expect(res.json()).resolves.toEqual({
      ok: true,
      trialExpiryAckAt: '2026-07-01T09:30:00.000Z',
    })
  })

  it('returns the previously-stamped timestamp when already acknowledged', async () => {
    mockAckTrialExpiry.mockResolvedValueOnce(undefined)
    mockGetSubscriptionByTenantId.mockResolvedValueOnce({
      trialExpiryAckAt: new Date('2026-07-01T09:30:00.000Z'),
    })

    const res = await appWithRoute().request(
      '/api/zync-subscription/ack-trial-expiry',
      {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({}),
      },
      mockEnv,
    )

    expect(res.status).toBe(200)
    await expect(res.json()).resolves.toEqual({
      ok: true,
      trialExpiryAckAt: '2026-07-01T09:30:00.000Z',
    })
  })
})

describe('DELETE /api/zync-subscription', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockSession = OWNER_SESSION
    mockGetSubscriptionByTenantId.mockResolvedValue({
      id: 'sub-1',
      tenantId: 'tenant-id',
      tier: 'business',
      status: 'active',
      period: 'monthly',
      adapter: 'stripe',
      currentPeriodEnd: new Date('2099-08-01T00:00:00.000Z'),
      canceledAt: null,
    })
    mockMarkSubscriptionCanceled.mockResolvedValue({
      id: 'sub-1',
      tenantId: 'tenant-id',
      tier: 'business',
      status: 'canceled',
      period: 'monthly',
      currentPeriodEnd: new Date('2099-08-01T00:00:00.000Z'),
      canceledAt: new Date('2026-07-02T10:00:00.000Z'),
      cancellationReason: 'too_expensive',
      cancellationReasonFreetext: null,
    })
    mockCancelSubscription.mockResolvedValue({
      effectiveDate: '2026-08-01T00:00:00.000Z',
    })
    mockGetUserEmailAndLocale.mockResolvedValue({
      email: 'owner@example.com',
      locale: 'en-US',
    })
    mockGetTenantById.mockResolvedValue({
      settings: { locale: 'en' },
    })
  })

  it('stores optional survey fields, marks the subscription canceled, and returns canceled state', async () => {
    const res = await appWithRoute().request(
      '/api/zync-subscription',
      {
        method: 'DELETE',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          cancellationReason: 'too_expensive',
        }),
      },
      mockEnv,
    )

    expect(res.status).toBe(200)
    expect(mockMarkSubscriptionCanceled).toHaveBeenCalledWith(
      {},
      'tenant-id',
      expect.objectContaining({
        cancellationReason: 'too_expensive',
      }),
    )
    await expect(res.json()).resolves.toEqual({
      status: 'canceled',
      canceledAt: '2026-07-02T10:00:00.000Z',
      periodEnd: '2026-08-01T00:00:00.000Z',
    })
  })

  it('rejects non-owner cancellation even if the user is an admin', async () => {
    mockSession = { ...OWNER_SESSION, role: 'ADMIN' }

    const res = await appWithRoute().request(
      '/api/zync-subscription',
      {
        method: 'DELETE',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({}),
      },
      mockEnv,
    )

    expect(res.status).toBe(403)
    expect(mockMarkSubscriptionCanceled).not.toHaveBeenCalled()
  })
})

describe('POST /api/zync-subscription/reactivate', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockSession = OWNER_SESSION
    mockGetSubscriptionByTenantId.mockResolvedValue({
      id: 'sub-1',
      tenantId: 'tenant-id',
      tier: 'business',
      status: 'canceled',
      period: 'monthly',
      adapter: 'stripe',
      currentPeriodEnd: new Date('2099-08-01T00:00:00.000Z'),
      canceledAt: new Date('2026-07-02T10:00:00.000Z'),
    })
    mockReactivateCanceledSubscription.mockResolvedValue({
      id: 'sub-1',
      tenantId: 'tenant-id',
      tier: 'business',
      status: 'active',
      period: 'monthly',
      currentPeriodEnd: new Date('2099-08-01T00:00:00.000Z'),
      canceledAt: null,
    })
  })

  it('reactivates a canceled subscription before period end', async () => {
    const res = await appWithRoute().request(
      '/api/zync-subscription/reactivate',
      {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({}),
      },
      mockEnv,
    )

    expect(res.status).toBe(200)
    expect(mockReactivateCanceledSubscription).toHaveBeenCalledWith({}, 'tenant-id')
    await expect(res.json()).resolves.toEqual({
      status: 'active',
      canceledAt: null,
    })
  })
})
