/**
 * @vitest-environment node
 */
import {
  auditMock,
  authMock,
  authMockFns,
  createMockRequest,
  permissionsMock,
  permissionsMockFns,
  posthogServerMock,
  schemaMock,
} from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const {
  mockGetWorkspaceInvitePolicy,
  mockValidateInvitationsAllowed,
  mockValidateSeatAvailability,
  mockGetUserOrganization,
  mockCreatePendingInvitation,
  mockSendInvitationEmail,
  mockCancelPendingInvitation,
  mockFindPendingGrantForWorkspaceEmail,
  mockDbResults,
} = vi.hoisted(() => ({
  mockGetWorkspaceInvitePolicy: vi.fn(),
  mockValidateInvitationsAllowed: vi.fn().mockResolvedValue(undefined),
  mockValidateSeatAvailability: vi.fn(),
  mockGetUserOrganization: vi.fn(),
  mockCreatePendingInvitation: vi.fn(),
  mockSendInvitationEmail: vi.fn(),
  mockCancelPendingInvitation: vi.fn(),
  mockFindPendingGrantForWorkspaceEmail: vi.fn(),
  mockDbResults: { value: [] as any[] },
}))

vi.mock('@sim/db', () => ({
  db: {
    select: vi.fn().mockImplementation(() => {
      const chain: any = {}
      chain.from = vi.fn().mockReturnValue(chain)
      chain.innerJoin = vi.fn().mockReturnValue(chain)
      chain.where = vi.fn().mockReturnValue(chain)
      chain.limit = vi
        .fn()
        .mockImplementation(() => Promise.resolve(mockDbResults.value.shift() || []))
      chain.then = vi.fn().mockImplementation((callback: (rows: any[]) => unknown) => {
        const result = mockDbResults.value.shift() || []
        return Promise.resolve(callback ? callback(result) : result)
      })
      return chain
    }),
  },
}))

vi.mock('@sim/db/schema', () => schemaMock)

vi.mock('@/lib/auth', () => authMock)

vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)

vi.mock('@/lib/workspaces/policy', () => ({
  getWorkspaceInvitePolicy: mockGetWorkspaceInvitePolicy,
  isOrganizationWorkspace: (ws: {
    workspaceMode?: string | null
    organizationId?: string | null
  }) => ws.workspaceMode === 'organization' && !!ws.organizationId,
}))

vi.mock('@/lib/billing/validation/seat-management', () => ({
  validateSeatAvailability: mockValidateSeatAvailability,
}))

vi.mock('@/lib/billing/organizations/membership', () => ({
  getUserOrganization: mockGetUserOrganization,
}))

vi.mock('@/lib/invitations/send', () => ({
  createPendingInvitation: mockCreatePendingInvitation,
  sendInvitationEmail: mockSendInvitationEmail,
  cancelPendingInvitation: mockCancelPendingInvitation,
  findPendingGrantForWorkspaceEmail: mockFindPendingGrantForWorkspaceEmail,
}))

vi.mock('@/lib/invitations/core', () => ({
  normalizeEmail: (email: string) => email.trim().toLowerCase(),
  listInvitationsForWorkspaces: vi.fn().mockResolvedValue([]),
}))

vi.mock('@/ee/access-control/utils/permission-check', () => ({
  validateInvitationsAllowed: mockValidateInvitationsAllowed,
  InvitationsNotAllowedError: class InvitationsNotAllowedError extends Error {},
}))

vi.mock('@sim/audit', () => auditMock)

vi.mock('@/lib/posthog/server', () => posthogServerMock)

vi.mock('@/lib/core/telemetry', () => ({
  PlatformEvents: {
    workspaceMemberInvited: vi.fn(),
  },
}))

const mockGetSession = authMockFns.mockGetSession
const mockGetWorkspaceWithOwner = permissionsMockFns.mockGetWorkspaceWithOwner

import { UPGRADE_TO_INVITE_REASON } from '@/lib/workspaces/policy-constants'
import { POST } from '@/app/api/workspaces/invitations/batch/route'

describe('POST /api/workspaces/invitations/batch', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockDbResults.value = []
    mockGetSession.mockResolvedValue({
      user: { id: 'user-1', email: 'owner@test.com', name: 'Owner User' },
    })
    mockGetWorkspaceWithOwner.mockResolvedValue({
      id: 'workspace-1',
      name: 'Workspace',
      ownerId: 'user-1',
      organizationId: null,
      workspaceMode: 'grandfathered_shared',
      billedAccountUserId: 'user-1',
    })
    permissionsMockFns.mockHasWorkspaceAdminAccess.mockResolvedValue(true)
    mockValidateInvitationsAllowed.mockResolvedValue(undefined)
    mockGetWorkspaceInvitePolicy.mockResolvedValue({
      allowed: true,
      reason: null,
      requiresSeat: false,
      organizationId: null,
      upgradeRequired: false,
    })
    mockValidateSeatAvailability.mockResolvedValue({
      canInvite: true,
      currentSeats: 1,
      maxSeats: 5,
      availableSeats: 4,
    })
    mockGetUserOrganization.mockResolvedValue(null)
    mockCreatePendingInvitation.mockResolvedValue({
      invitationId: 'inv-1',
      token: 'tok-1',
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
    })
    mockSendInvitationEmail.mockResolvedValue({ success: true })
    mockFindPendingGrantForWorkspaceEmail.mockResolvedValue(null)
  })

  it('blocks invites for personal workspaces with an upgrade prompt', async () => {
    mockGetWorkspaceWithOwner.mockResolvedValueOnce({
      id: 'workspace-1',
      name: 'Personal Workspace',
      ownerId: 'user-1',
      organizationId: null,
      workspaceMode: 'personal',
      billedAccountUserId: 'user-1',
    })
    mockGetWorkspaceInvitePolicy.mockResolvedValueOnce({
      allowed: false,
      reason: UPGRADE_TO_INVITE_REASON,
      requiresSeat: false,
      organizationId: null,
      upgradeRequired: true,
    })
    mockDbResults.value = []

    const request = createMockRequest('POST', {
      workspaceId: 'workspace-1',
      invitations: [{ email: 'new@example.com', permission: 'read' }],
    })

    const response = await POST(request)
    const data = await response.json()

    expect(response.status).toBe(403)
    expect(data.error).toBe(UPGRADE_TO_INVITE_REASON)
    expect(data.upgradeRequired).toBe(true)
  })

  it('blocks invites for grandfathered workspaces without a team plan', async () => {
    mockGetWorkspaceWithOwner.mockResolvedValueOnce({
      id: 'workspace-1',
      name: 'Grandfathered Workspace',
      ownerId: 'user-1',
      organizationId: null,
      workspaceMode: 'grandfathered_shared',
      billedAccountUserId: 'user-1',
    })
    mockGetWorkspaceInvitePolicy.mockResolvedValueOnce({
      allowed: false,
      reason: UPGRADE_TO_INVITE_REASON,
      requiresSeat: false,
      organizationId: null,
      upgradeRequired: true,
    })
    mockDbResults.value = []

    const request = createMockRequest('POST', {
      workspaceId: 'workspace-1',
      invitations: [{ email: 'new@example.com', permission: 'read' }],
    })

    const response = await POST(request)
    const data = await response.json()

    expect(response.status).toBe(403)
    expect(data.upgradeRequired).toBe(true)
    expect(mockCreatePendingInvitation).not.toHaveBeenCalled()
  })

  it('reports org-owned invites as failed when the organization has no available seats', async () => {
    mockGetWorkspaceWithOwner.mockResolvedValueOnce({
      id: 'workspace-1',
      name: 'Org Workspace',
      ownerId: 'user-1',
      organizationId: 'org-1',
      workspaceMode: 'organization',
      billedAccountUserId: 'owner-1',
    })
    mockGetWorkspaceInvitePolicy.mockResolvedValueOnce({
      allowed: true,
      reason: null,
      requiresSeat: true,
      organizationId: 'org-1',
      upgradeRequired: false,
    })
    mockValidateSeatAvailability.mockResolvedValueOnce({
      canInvite: false,
      reason: 'No available seats. Currently using 5 of 5 seats.',
      currentSeats: 5,
      maxSeats: 5,
      availableSeats: 0,
    })
    mockDbResults.value = [[]]

    const request = createMockRequest('POST', {
      workspaceId: 'workspace-1',
      invitations: [{ email: 'new@example.com', permission: 'read' }],
    })

    const response = await POST(request)
    const data = await response.json()

    expect(response.status).toBe(200)
    expect(data.success).toBe(false)
    expect(data.failed).toEqual([
      {
        email: 'new@example.com',
        error: 'No available seats. Currently using 5 of 5 seats.',
      },
    ])
    expect(mockValidateSeatAvailability).toHaveBeenCalledWith('org-1', 1)
    expect(mockCreatePendingInvitation).not.toHaveBeenCalled()
  })

  it('creates an external workspace invitation for users already in another organization', async () => {
    mockGetWorkspaceWithOwner.mockResolvedValueOnce({
      id: 'workspace-1',
      name: 'Org Workspace',
      ownerId: 'user-1',
      organizationId: 'org-1',
      workspaceMode: 'organization',
      billedAccountUserId: 'owner-1',
    })
    mockGetWorkspaceInvitePolicy.mockResolvedValueOnce({
      allowed: true,
      reason: null,
      requiresSeat: true,
      organizationId: 'org-1',
      upgradeRequired: false,
    })
    mockGetUserOrganization.mockResolvedValueOnce({
      organizationId: 'org-2',
      role: 'member',
      memberId: 'member-1',
    })
    mockDbResults.value = [[{ id: 'existing-user', email: 'new@example.com' }]]

    const request = createMockRequest('POST', {
      workspaceId: 'workspace-1',
      invitations: [{ email: 'new@example.com', permission: 'read' }],
    })

    const response = await POST(request)
    const data = await response.json()

    expect(response.status).toBe(200)
    expect(data.success).toBe(true)
    expect(data.invitations[0].membershipIntent).toBe('external')
    expect(mockValidateSeatAvailability).not.toHaveBeenCalled()
    expect(mockCreatePendingInvitation).toHaveBeenCalledWith(
      expect.objectContaining({
        kind: 'workspace',
        email: 'new@example.com',
        organizationId: 'org-1',
        membershipIntent: 'external',
        grants: [{ workspaceId: 'workspace-1', permission: 'read' }],
      })
    )
  })

  it('creates a unified workspace invitation for a grandfathered workspace', async () => {
    mockGetWorkspaceWithOwner.mockResolvedValueOnce({
      id: 'workspace-1',
      name: 'Grandfathered Workspace',
      ownerId: 'user-1',
      organizationId: null,
      workspaceMode: 'grandfathered_shared',
      billedAccountUserId: 'user-1',
    })
    mockDbResults.value = [[]]

    const request = createMockRequest('POST', {
      workspaceId: 'workspace-1',
      invitations: [{ email: 'new@example.com', permission: 'write' }],
    })

    const response = await POST(request)
    const data = await response.json()

    expect(response.status).toBe(200)
    expect(data.success).toBe(true)
    expect(mockCreatePendingInvitation).toHaveBeenCalledWith(
      expect.objectContaining({
        kind: 'workspace',
        email: 'new@example.com',
        organizationId: null,
        grants: [{ workspaceId: 'workspace-1', permission: 'write' }],
      })
    )
    expect(mockSendInvitationEmail).toHaveBeenCalled()
    expect(mockValidateSeatAvailability).not.toHaveBeenCalled()
  })

  it('creates multiple workspace invitations in one batch request', async () => {
    mockDbResults.value = [[], []]
    mockCreatePendingInvitation
      .mockResolvedValueOnce({
        invitationId: 'inv-1',
        token: 'tok-1',
        expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
      })
      .mockResolvedValueOnce({
        invitationId: 'inv-2',
        token: 'tok-2',
        expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
      })

    const request = createMockRequest('POST', {
      workspaceId: 'workspace-1',
      invitations: [
        { email: 'first@example.com', permission: 'read' },
        { email: 'second@example.com', permission: 'write' },
      ],
    })

    const response = await POST(request)
    const data = await response.json()

    expect(response.status).toBe(200)
    expect(data.success).toBe(true)
    expect(data.successful).toEqual(['first@example.com', 'second@example.com'])
    expect(data.failed).toEqual([])
    expect(data.invitations).toHaveLength(2)
    expect(mockCreatePendingInvitation).toHaveBeenCalledTimes(2)
    expect(mockSendInvitationEmail).toHaveBeenCalledTimes(2)
  })

  it('rolls back the unified invitation when email delivery fails', async () => {
    mockGetWorkspaceWithOwner.mockResolvedValueOnce({
      id: 'workspace-1',
      name: 'Org Workspace',
      ownerId: 'user-1',
      organizationId: 'org-1',
      workspaceMode: 'organization',
      billedAccountUserId: 'owner-1',
    })
    mockSendInvitationEmail.mockResolvedValueOnce({
      success: false,
      error: 'mailer unavailable',
    })
    mockDbResults.value = [[]]

    const request = createMockRequest('POST', {
      workspaceId: 'workspace-1',
      invitations: [{ email: 'new@example.com', permission: 'read' }],
    })

    const response = await POST(request)

    expect(response.status).toBe(200)
    await expect(response.json()).resolves.toEqual(
      expect.objectContaining({
        success: false,
        failed: [{ email: 'new@example.com', error: 'mailer unavailable' }],
      })
    )
    expect(mockCancelPendingInvitation).toHaveBeenCalledWith('inv-1')
  })
})
