/**
 * @vitest-environment node
 */
import { auditMock, createMockRequest, createSession, loggerMock } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const {
  mockDbState,
  mockGetSession,
  mockValidateInvitationsAllowed,
  mockValidateSeatAvailability,
  mockCreatePendingInvitation,
  mockSendInvitationEmail,
  mockCancelPendingInvitation,
  mockGrantWorkspaceAccessDirectly,
} = vi.hoisted(() => ({
  mockDbState: {
    selectResults: [] as any[],
  },
  mockGetSession: vi.fn(),
  mockValidateInvitationsAllowed: vi.fn(),
  mockValidateSeatAvailability: vi.fn(),
  mockCreatePendingInvitation: vi.fn(),
  mockSendInvitationEmail: vi.fn(),
  mockCancelPendingInvitation: vi.fn(),
  mockGrantWorkspaceAccessDirectly: vi.fn(),
}))

function createSelectChain() {
  const chain: any = {}
  chain.from = vi.fn().mockReturnValue(chain)
  chain.innerJoin = vi.fn().mockReturnValue(chain)
  chain.leftJoin = vi.fn().mockReturnValue(chain)
  chain.where = vi.fn().mockReturnValue(chain)
  chain.orderBy = vi.fn().mockReturnValue(chain)
  chain.limit = vi
    .fn()
    .mockImplementation(() => Promise.resolve(mockDbState.selectResults.shift() ?? []))
  chain.then = vi.fn().mockImplementation((callback: (rows: any[]) => unknown) => {
    const rows = mockDbState.selectResults.shift() ?? []
    return Promise.resolve(callback(rows))
  })
  return chain
}

vi.mock('@sim/db', () => ({
  db: {
    select: vi.fn().mockImplementation(() => createSelectChain()),
  },
}))

vi.mock('@sim/db/schema', () => ({
  invitation: {
    id: 'invitation.id',
    organizationId: 'invitation.organizationId',
    status: 'invitation.status',
    email: 'invitation.email',
    kind: 'invitation.kind',
    role: 'invitation.role',
    inviterId: 'invitation.inviterId',
    expiresAt: 'invitation.expiresAt',
    createdAt: 'invitation.createdAt',
  },
  member: {
    organizationId: 'member.organizationId',
    userId: 'member.userId',
    role: 'member.role',
  },
  organization: {
    id: 'organization.id',
    name: 'organization.name',
  },
  user: {
    id: 'user.id',
    name: 'user.name',
    email: 'user.email',
  },
  workspace: {
    id: 'workspace.id',
    name: 'workspace.name',
    organizationId: 'workspace.organizationId',
    workspaceMode: 'workspace.workspaceMode',
  },
  permissions: {
    entityId: 'permissions.entityId',
    entityType: 'permissions.entityType',
    userId: 'permissions.userId',
  },
  invitationWorkspaceGrant: {
    invitationId: 'invitationWorkspaceGrant.invitationId',
    workspaceId: 'invitationWorkspaceGrant.workspaceId',
  },
}))

vi.mock('drizzle-orm', () => ({
  and: vi.fn((...conditions: unknown[]) => ({ type: 'and', conditions })),
  eq: vi.fn((field: unknown, value: unknown) => ({ field, value })),
  inArray: vi.fn((field: unknown, values: unknown[]) => ({ field, values })),
}))

vi.mock('@sim/logger', () => loggerMock)

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

vi.mock('@/lib/auth', () => ({
  getSession: mockGetSession,
}))

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

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

vi.mock('@/lib/invitations/direct-grant', () => ({
  grantWorkspaceAccessDirectly: mockGrantWorkspaceAccessDirectly,
}))

vi.mock('@/lib/messaging/email/validation', () => ({
  quickValidateEmail: vi.fn((email: string) => ({ isValid: email.includes('@') })),
}))

vi.mock('@/lib/workspaces/permissions/utils', () => ({
  hasWorkspaceAdminAccess: vi.fn().mockResolvedValue(true),
}))

vi.mock('@/lib/workspaces/policy', () => ({
  isOrganizationWorkspace: vi.fn().mockReturnValue(true),
}))

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

import { POST } from '@/app/api/organizations/[id]/invitations/route'

describe('POST /api/organizations/[id]/invitations', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockDbState.selectResults = []
    mockValidateInvitationsAllowed.mockResolvedValue(undefined)
    mockValidateSeatAvailability.mockResolvedValue({
      canInvite: true,
      currentSeats: 1,
      maxSeats: 5,
      availableSeats: 4,
    })
    mockCreatePendingInvitation.mockResolvedValue({
      invitationId: 'inv-1',
      token: 'tok-1',
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
    })
    mockSendInvitationEmail.mockResolvedValue({ success: true })
    mockGrantWorkspaceAccessDirectly.mockResolvedValue({ outcome: 'added', permission: 'write' })
  })

  it('creates a unified invitation and sends a single email', async () => {
    mockGetSession.mockResolvedValue(
      createSession({ userId: 'user-1', email: 'owner@example.com', name: 'Owner' })
    )
    mockDbState.selectResults = [
      [{ role: 'owner' }],
      [{ name: 'Org One' }],
      [],
      [],
      [{ name: 'Owner', email: 'owner@example.com' }],
    ]

    const response = await POST(
      createMockRequest(
        'POST',
        { emails: ['invitee@example.com'] },
        {},
        'http://localhost/api/organizations/org-1/invitations'
      ),
      { params: Promise.resolve({ id: 'org-1' }) }
    )

    expect(response.status).toBe(200)
    expect(mockCreatePendingInvitation).toHaveBeenCalledWith(
      expect.objectContaining({
        kind: 'organization',
        email: 'invitee@example.com',
        organizationId: 'org-1',
        role: 'member',
        grants: [],
      })
    )
    expect(mockSendInvitationEmail).toHaveBeenCalledWith(
      expect.objectContaining({ kind: 'organization', email: 'invitee@example.com' })
    )
    expect(mockCancelPendingInvitation).not.toHaveBeenCalled()
  })

  it('adds an existing member directly to selected workspaces they lack (no invitation/email)', async () => {
    mockGetSession.mockResolvedValue(
      createSession({ userId: 'user-1', email: 'owner@example.com', name: 'Owner' })
    )
    mockDbState.selectResults = [
      [{ role: 'owner' }],
      [{ name: 'Org One' }],
      [{ id: 'ws-1', name: 'Workspace 1', organizationId: 'org-1', workspaceMode: 'organization' }],
      [{ id: 'ws-2', name: 'Workspace 2', organizationId: 'org-1', workspaceMode: 'organization' }],
      [{ userId: 'user-2', userEmail: 'member@example.com' }],
      [],
      [{ userId: 'user-2', workspaceId: 'ws-1' }],
      [],
      [{ name: 'Owner', email: 'owner@example.com' }],
    ]

    const response = await POST(
      createMockRequest(
        'POST',
        {
          emails: ['member@example.com'],
          workspaceInvitations: [
            { workspaceId: 'ws-1', permission: 'write' },
            { workspaceId: 'ws-2', permission: 'write' },
          ],
        },
        {},
        'http://localhost/api/organizations/org-1/invitations?batch=true'
      ),
      { params: Promise.resolve({ id: 'org-1' }) }
    )

    expect(response.status).toBe(200)
    expect(mockCreatePendingInvitation).not.toHaveBeenCalled()
    expect(mockSendInvitationEmail).not.toHaveBeenCalled()
    expect(mockGrantWorkspaceAccessDirectly).toHaveBeenCalledTimes(1)
    expect(mockGrantWorkspaceAccessDirectly).toHaveBeenCalledWith(
      expect.objectContaining({
        userId: 'user-2',
        email: 'member@example.com',
        workspaceId: 'ws-2',
        permission: 'write',
        organizationId: 'org-1',
      })
    )

    const body = await response.json()
    expect(body.data.invitationsSent).toBe(0)
    expect(body.data.directlyAdded).toEqual(['member@example.com'])
    expect(body.data.directlyAddedCount).toBe(1)
    expect(body.data.existingMembers).toEqual([])
  })

  it('reports a partially-failed member only as added, never in both buckets', async () => {
    mockGetSession.mockResolvedValue(
      createSession({ userId: 'user-1', email: 'owner@example.com', name: 'Owner' })
    )
    // First grant succeeds, second throws (e.g. transient DB error).
    mockGrantWorkspaceAccessDirectly
      .mockResolvedValueOnce({ outcome: 'added', permission: 'write' })
      .mockRejectedValueOnce(new Error('db blip'))
    mockDbState.selectResults = [
      [{ role: 'owner' }],
      [{ name: 'Org One' }],
      [{ id: 'ws-1', name: 'Workspace 1', organizationId: 'org-1', workspaceMode: 'organization' }],
      [{ id: 'ws-2', name: 'Workspace 2', organizationId: 'org-1', workspaceMode: 'organization' }],
      [{ userId: 'user-2', userEmail: 'member@example.com' }],
      [],
      [],
      [],
      [{ name: 'Owner', email: 'owner@example.com' }],
    ]

    const response = await POST(
      createMockRequest(
        'POST',
        {
          emails: ['member@example.com'],
          workspaceInvitations: [
            { workspaceId: 'ws-1', permission: 'write' },
            { workspaceId: 'ws-2', permission: 'write' },
          ],
        },
        {},
        'http://localhost/api/organizations/org-1/invitations?batch=true'
      ),
      { params: Promise.resolve({ id: 'org-1' }) }
    )

    expect(response.status).toBe(200)
    expect(mockGrantWorkspaceAccessDirectly).toHaveBeenCalledTimes(2)
    const body = await response.json()
    expect(body.data.directlyAdded).toEqual(['member@example.com'])
    expect(body.data.failedInvitations).toEqual([])
  })

  it('returns 207 with both successes and failures when one member is added and another fails', async () => {
    mockGetSession.mockResolvedValue(
      createSession({ userId: 'user-1', email: 'owner@example.com', name: 'Owner' })
    )
    mockGrantWorkspaceAccessDirectly
      .mockResolvedValueOnce({ outcome: 'added', permission: 'write' })
      .mockRejectedValueOnce(new Error('db blip'))
    mockDbState.selectResults = [
      [{ role: 'owner' }],
      [{ name: 'Org One' }],
      [{ id: 'ws-1', name: 'Workspace 1', organizationId: 'org-1', workspaceMode: 'organization' }],
      [
        { userId: 'user-a', userEmail: 'a@example.com' },
        { userId: 'user-b', userEmail: 'b@example.com' },
      ],
      [],
      [],
      [],
      [{ name: 'Owner', email: 'owner@example.com' }],
    ]

    const response = await POST(
      createMockRequest(
        'POST',
        {
          emails: ['a@example.com', 'b@example.com'],
          workspaceInvitations: [{ workspaceId: 'ws-1', permission: 'write' }],
        },
        {},
        'http://localhost/api/organizations/org-1/invitations?batch=true'
      ),
      { params: Promise.resolve({ id: 'org-1' }) }
    )

    expect(response.status).toBe(207)
    const body = await response.json()
    expect(body.success).toBe(false)
    expect(body.data.directlyAdded).toEqual(['a@example.com'])
    expect(body.data.directlyAddedCount).toBe(1)
    expect(body.data.failedInvitations).toEqual([{ email: 'b@example.com', error: 'db blip' }])
  })

  it('returns 400 when an existing member already has access to every selected workspace', async () => {
    mockGetSession.mockResolvedValue(
      createSession({ userId: 'user-1', email: 'owner@example.com', name: 'Owner' })
    )
    mockDbState.selectResults = [
      [{ role: 'owner' }],
      [{ name: 'Org One' }],
      [{ id: 'ws-1', organizationId: 'org-1', workspaceMode: 'organization' }],
      [{ userId: 'user-2', userEmail: 'member@example.com' }],
      [],
      [{ userId: 'user-2', workspaceId: 'ws-1' }],
      [],
    ]

    const response = await POST(
      createMockRequest(
        'POST',
        {
          emails: ['member@example.com'],
          workspaceInvitations: [{ workspaceId: 'ws-1', permission: 'write' }],
        },
        {},
        'http://localhost/api/organizations/org-1/invitations?batch=true'
      ),
      { params: Promise.resolve({ id: 'org-1' }) }
    )

    expect(response.status).toBe(400)
    const body = await response.json()
    expect(body.error).toContain('already has access')
    expect(mockCreatePendingInvitation).not.toHaveBeenCalled()
  })

  it('invites new emails to the organization and adds existing members to workspaces in one batch', async () => {
    mockGetSession.mockResolvedValue(
      createSession({ userId: 'user-1', email: 'owner@example.com', name: 'Owner' })
    )
    mockDbState.selectResults = [
      [{ role: 'owner' }],
      [{ name: 'Org One' }],
      [{ id: 'ws-1', name: 'Workspace 1', organizationId: 'org-1', workspaceMode: 'organization' }],
      [{ userId: 'user-2', userEmail: 'member@example.com' }],
      [],
      [],
      [],
      [{ name: 'Owner', email: 'owner@example.com' }],
    ]

    const response = await POST(
      createMockRequest(
        'POST',
        {
          emails: ['new@example.com', 'member@example.com'],
          workspaceInvitations: [{ workspaceId: 'ws-1', permission: 'read' }],
        },
        {},
        'http://localhost/api/organizations/org-1/invitations?batch=true'
      ),
      { params: Promise.resolve({ id: 'org-1' }) }
    )

    expect(response.status).toBe(200)
    expect(mockCreatePendingInvitation).toHaveBeenCalledTimes(1)
    expect(mockCreatePendingInvitation).toHaveBeenCalledWith(
      expect.objectContaining({
        kind: 'organization',
        email: 'new@example.com',
        grants: [{ workspaceId: 'ws-1', permission: 'read' }],
      })
    )
    expect(mockGrantWorkspaceAccessDirectly).toHaveBeenCalledTimes(1)
    expect(mockGrantWorkspaceAccessDirectly).toHaveBeenCalledWith(
      expect.objectContaining({
        userId: 'user-2',
        email: 'member@example.com',
        workspaceId: 'ws-1',
        permission: 'read',
      })
    )

    const body = await response.json()
    expect(body.data.invitationsSent).toBe(1)
    expect(body.data.invitedEmails).toEqual(['new@example.com'])
    expect(body.data.directlyAdded).toEqual(['member@example.com'])
    expect(body.data.directlyAddedCount).toBe(1)
  })

  it('still rejects existing members on the non-batch organization invite path', async () => {
    mockGetSession.mockResolvedValue(
      createSession({ userId: 'user-1', email: 'owner@example.com', name: 'Owner' })
    )
    mockDbState.selectResults = [
      [{ role: 'owner' }],
      [{ name: 'Org One' }],
      [{ userId: 'user-2', userEmail: 'member@example.com' }],
      [],
    ]

    const response = await POST(
      createMockRequest(
        'POST',
        { emails: ['member@example.com'] },
        {},
        'http://localhost/api/organizations/org-1/invitations'
      ),
      { params: Promise.resolve({ id: 'org-1' }) }
    )

    expect(response.status).toBe(400)
    const body = await response.json()
    expect(body.error).toBe(
      'Failed to send invitation. User is already a part of the organization.'
    )
    expect(mockCreatePendingInvitation).not.toHaveBeenCalled()
  })

  it('rolls back the pending invitation when email delivery fails', async () => {
    mockGetSession.mockResolvedValue(
      createSession({ userId: 'user-1', email: 'owner@example.com', name: 'Owner' })
    )
    mockDbState.selectResults = [
      [{ role: 'owner' }],
      [{ name: 'Org One' }],
      [],
      [],
      [{ name: 'Owner', email: 'owner@example.com' }],
    ]
    mockSendInvitationEmail.mockResolvedValue({ success: false, error: 'mailer unavailable' })

    const response = await POST(
      createMockRequest(
        'POST',
        { emails: ['invitee@example.com'] },
        {},
        'http://localhost/api/organizations/org-1/invitations'
      ),
      { params: Promise.resolve({ id: 'org-1' }) }
    )

    expect(response.status).toBe(502)
    expect(mockCancelPendingInvitation).toHaveBeenCalledWith('inv-1')
  })
})
