/**
 * Tests for chat API utils
 *
 * @vitest-environment node
 */
import {
  dbChainMock,
  dbChainMockFns,
  encryptionMock,
  encryptionMockFns,
  loggingSessionMock,
  workflowsUtilsMock,
} from '@sim/testing'
import type { NextResponse } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const {
  mockMergeSubblockStateWithValues,
  mockMergeSubBlockValues,
  mockValidateAuthToken,
  mockSetDeploymentAuthCookie,
  mockIsEmailAllowed,
  mockGetSession,
  mockCheckRateLimitDirect,
  mockIsWorkspaceApiExecutionEntitled,
  flagState,
} = vi.hoisted(() => ({
  mockMergeSubblockStateWithValues: vi.fn().mockReturnValue({}),
  mockMergeSubBlockValues: vi.fn().mockReturnValue({}),
  mockValidateAuthToken: vi.fn().mockReturnValue(false),
  mockSetDeploymentAuthCookie: vi.fn(),
  mockIsEmailAllowed: vi.fn(),
  mockGetSession: vi.fn(),
  mockCheckRateLimitDirect: vi.fn().mockResolvedValue({ allowed: true }),
  mockIsWorkspaceApiExecutionEntitled: vi.fn().mockResolvedValue(true),
  flagState: { isBillingEnabled: false, isFreeApiDeploymentGateEnabled: true },
}))

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

vi.mock('@/lib/billing/core/api-access', () => ({
  isWorkspaceApiExecutionEntitled: mockIsWorkspaceApiExecutionEntitled,
}))

vi.mock('@/lib/core/rate-limiter', () => ({
  RateLimiter: class {
    checkRateLimitDirect = mockCheckRateLimitDirect
  },
}))

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

const mockDecryptSecret = encryptionMockFns.mockDecryptSecret

vi.mock('@/lib/logs/execution/logging-session', () => loggingSessionMock)

vi.mock('@/executor', () => ({
  Executor: vi.fn(),
}))

vi.mock('@/serializer', () => ({
  Serializer: vi.fn(),
}))

vi.mock('@sim/workflow-persistence/subblocks', () => ({
  mergeSubblockStateWithValues: mockMergeSubblockStateWithValues,
  mergeSubBlockValues: mockMergeSubBlockValues,
}))

vi.mock('@/lib/core/security/encryption', () => encryptionMock)

vi.mock('@/lib/core/security/deployment', () => ({
  validateAuthToken: mockValidateAuthToken,
  setDeploymentAuthCookie: mockSetDeploymentAuthCookie,
  isEmailAllowed: mockIsEmailAllowed,
  deploymentAuthCookieName: (prefix: string, id: string) => `${prefix}_auth_${id}`,
}))

vi.mock('@/lib/core/config/env-flags', () => ({
  isDev: true,
  isProd: false,
  get isBillingEnabled() {
    return flagState.isBillingEnabled
  },
  get isFreeApiDeploymentGateEnabled() {
    return flagState.isFreeApiDeploymentGateEnabled
  },
}))

vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)

import { NextRequest } from 'next/server'
import { decryptSecret } from '@/lib/core/security/encryption'
import { assertChatEmbedAllowed, setChatAuthCookie, validateChatAuth } from '@/app/api/chat/utils'

function chatRequest(origin?: string): NextRequest {
  return new NextRequest('https://www.sim.ai/api/chat/abc', {
    headers: origin ? { origin } : undefined,
  })
}

describe('Chat API Utils', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    vi.stubGlobal('process', {
      ...process,
      env: {
        ...process.env,
        NODE_ENV: 'development',
      },
    })
  })

  describe('Auth token utils', () => {
    it('should accept valid auth cookie via validateChatAuth', async () => {
      mockValidateAuthToken.mockReturnValue(true)

      const deployment = {
        id: 'chat-id',
        authType: 'password',
        password: 'encrypted-password',
      }

      const mockRequest = {
        method: 'POST',
        cookies: {
          get: vi.fn().mockReturnValue({ value: 'valid-token' }),
        },
      } as any

      const result = await validateChatAuth('request-id', deployment, mockRequest)
      expect(mockValidateAuthToken).toHaveBeenCalledWith(
        'valid-token',
        'chat-id',
        'password',
        'encrypted-password'
      )
      expect(result.authorized).toBe(true)
    })

    it('should reject invalid auth cookie via validateChatAuth', async () => {
      mockValidateAuthToken.mockReturnValue(false)

      const deployment = {
        id: 'chat-id',
        authType: 'password',
        password: 'encrypted-password',
      }

      const mockRequest = {
        method: 'GET',
        cookies: {
          get: vi.fn().mockReturnValue({ value: 'invalid-token' }),
        },
      } as any

      const result = await validateChatAuth('request-id', deployment, mockRequest)
      expect(result.authorized).toBe(false)
    })
  })

  describe('Cookie handling', () => {
    it('should delegate to setDeploymentAuthCookie', () => {
      const mockResponse = {
        cookies: { set: vi.fn() },
      } as unknown as NextResponse

      setChatAuthCookie(mockResponse, 'test-chat-id', 'password')

      expect(mockSetDeploymentAuthCookie).toHaveBeenCalledWith(
        mockResponse,
        'chat',
        'test-chat-id',
        'password',
        undefined
      )
    })
  })

  describe('Chat auth validation', () => {
    beforeEach(() => {
      mockDecryptSecret.mockResolvedValue({ decrypted: 'correct-password' })
      mockCheckRateLimitDirect.mockResolvedValue({ allowed: true })
    })

    it('should allow access to public chats', async () => {
      const deployment = {
        id: 'chat-id',
        authType: 'public',
      }

      const mockRequest = {
        cookies: {
          get: vi.fn().mockReturnValue(null),
        },
      } as any

      const result = await validateChatAuth('request-id', deployment, mockRequest)

      expect(result.authorized).toBe(true)
    })

    it('should request password auth for GET requests', async () => {
      const deployment = {
        id: 'chat-id',
        authType: 'password',
      }

      const mockRequest = {
        method: 'GET',
        cookies: {
          get: vi.fn().mockReturnValue(null),
        },
      } as any

      const result = await validateChatAuth('request-id', deployment, mockRequest)

      expect(result.authorized).toBe(false)
      expect(result.error).toBe('auth_required_password')
    })

    it('should validate password for POST requests', async () => {
      const deployment = {
        id: 'chat-id',
        authType: 'password',
        password: 'encrypted-password',
      }

      const mockRequest = {
        method: 'POST',
        cookies: {
          get: vi.fn().mockReturnValue(null),
        },
      } as any

      const parsedBody = {
        password: 'correct-password',
      }

      const result = await validateChatAuth('request-id', deployment, mockRequest, parsedBody)

      expect(decryptSecret).toHaveBeenCalledWith('encrypted-password')
      expect(result.authorized).toBe(true)
    })

    it('should reject incorrect password', async () => {
      const deployment = {
        id: 'chat-id',
        authType: 'password',
        password: 'encrypted-password',
      }

      const mockRequest = {
        method: 'POST',
        cookies: {
          get: vi.fn().mockReturnValue(null),
        },
      } as any

      const parsedBody = {
        password: 'wrong-password',
      }

      const result = await validateChatAuth('request-id', deployment, mockRequest, parsedBody)

      expect(result.authorized).toBe(false)
      expect(result.error).toBe('Invalid password')
    })

    it('should return 429 when the password attempt rate limit is exceeded', async () => {
      mockCheckRateLimitDirect.mockResolvedValueOnce({ allowed: false, retryAfterMs: 60_000 })

      const deployment = {
        id: 'chat-id',
        authType: 'password',
        password: 'encrypted-password',
      }

      const mockRequest = {
        method: 'POST',
        cookies: {
          get: vi.fn().mockReturnValue(null),
        },
      } as any

      const result = await validateChatAuth('request-id', deployment, mockRequest, {
        password: 'any-guess',
      })

      expect(result.authorized).toBe(false)
      expect(result.status).toBe(429)
      expect(result.retryAfterMs).toBe(60_000)
      expect(decryptSecret).not.toHaveBeenCalled()
    })

    it('should request email auth for email-protected chats', async () => {
      const deployment = {
        id: 'chat-id',
        authType: 'email',
        allowedEmails: ['user@example.com', '@company.com'],
      }

      const mockRequest = {
        method: 'GET',
        cookies: {
          get: vi.fn().mockReturnValue(null),
        },
      } as any

      const result = await validateChatAuth('request-id', deployment, mockRequest)

      expect(result.authorized).toBe(false)
      expect(result.error).toBe('auth_required_email')
    })

    it('should check allowed emails for email auth', async () => {
      const deployment = {
        id: 'chat-id',
        authType: 'email',
        allowedEmails: ['user@example.com', '@company.com'],
      }

      const mockRequest = {
        method: 'POST',
        cookies: {
          get: vi.fn().mockReturnValue(null),
        },
      } as any

      mockIsEmailAllowed.mockReturnValue(true)
      const result1 = await validateChatAuth('request-id', deployment, mockRequest, {
        email: 'user@example.com',
      })
      expect(result1.authorized).toBe(false)
      expect(result1.error).toBe('otp_required')

      const result2 = await validateChatAuth('request-id', deployment, mockRequest, {
        email: 'other@company.com',
      })
      expect(result2.authorized).toBe(false)
      expect(result2.error).toBe('otp_required')

      mockIsEmailAllowed.mockReturnValue(false)
      const result3 = await validateChatAuth('request-id', deployment, mockRequest, {
        email: 'user@unknown.com',
      })
      expect(result3.authorized).toBe(false)
      expect(result3.error).toBe('Email not authorized')
    })

    describe('SSO auth', () => {
      const ssoDeployment = {
        id: 'chat-id',
        authType: 'sso',
        allowedEmails: ['user@example.com', '@company.com'],
      }

      const postRequest = {
        method: 'POST',
        cookies: { get: vi.fn().mockReturnValue(null) },
      } as any

      it('rejects when no session is present', async () => {
        mockGetSession.mockResolvedValue(null)

        const result = await validateChatAuth('request-id', ssoDeployment, postRequest, {
          input: 'hello',
        })

        expect(result.authorized).toBe(false)
        expect(result.error).toBe('auth_required_sso')
      })

      it('ignores body-supplied email and uses the session email', async () => {
        mockGetSession.mockResolvedValue({ user: { email: 'session@example.com' } })
        mockIsEmailAllowed.mockReturnValue(true)

        await validateChatAuth('request-id', ssoDeployment, postRequest, {
          email: 'attacker@evil.com',
          input: 'hello',
        })

        expect(mockIsEmailAllowed).toHaveBeenCalledWith(
          'session@example.com',
          ssoDeployment.allowedEmails
        )
      })

      it('authorizes execution when session email is allowlisted', async () => {
        mockGetSession.mockResolvedValue({ user: { email: 'user@example.com' } })
        mockIsEmailAllowed.mockReturnValue(true)

        const result = await validateChatAuth('request-id', ssoDeployment, postRequest, {
          input: 'hello',
        })

        expect(result.authorized).toBe(true)
      })

      it('rejects execution when session email is not allowlisted', async () => {
        mockGetSession.mockResolvedValue({ user: { email: 'stranger@other.com' } })
        mockIsEmailAllowed.mockReturnValue(false)

        const result = await validateChatAuth('request-id', ssoDeployment, postRequest, {
          input: 'hello',
        })

        expect(result.authorized).toBe(false)
        expect(result.error).toBe('Your email is not authorized to access this resource')
      })
    })
  })

  describe('Execution Result Processing', () => {
    it.concurrent('should process logs regardless of overall success status', () => {
      const executionResult = {
        success: false,
        output: {},
        logs: [
          {
            blockId: 'agent1',
            startedAt: '2023-01-01T00:00:00Z',
            endedAt: '2023-01-01T00:00:01Z',
            durationMs: 1000,
            success: true,
            output: { content: 'Agent 1 succeeded' },
            error: undefined,
          },
          {
            blockId: 'agent2',
            startedAt: '2023-01-01T00:00:00Z',
            endedAt: '2023-01-01T00:00:01Z',
            durationMs: 500,
            success: false,
            output: null,
            error: 'Agent 2 failed',
          },
        ],
        metadata: { duration: 1000 },
      }

      expect(executionResult.success).toBe(false)
      expect(executionResult.logs).toBeDefined()
      expect(executionResult.logs).toHaveLength(2)

      expect(executionResult.logs[0].success).toBe(true)
      expect(executionResult.logs[0].output?.content).toBe('Agent 1 succeeded')

      expect(executionResult.logs[1].success).toBe(false)
      expect(executionResult.logs[1].error).toBe('Agent 2 failed')
    })

    it.concurrent('should handle ExecutionResult vs StreamingExecution types correctly', () => {
      const executionResult = {
        success: true,
        output: { content: 'test' },
        logs: [],
        metadata: { duration: 100 },
      }

      const directResult = executionResult
      const extractedDirect = directResult
      expect(extractedDirect).toBe(executionResult)

      const streamingResult = {
        stream: new ReadableStream(),
        execution: executionResult,
      }

      const extractedFromStreaming =
        streamingResult && typeof streamingResult === 'object' && 'execution' in streamingResult
          ? streamingResult.execution
          : streamingResult

      expect(extractedFromStreaming).toBe(executionResult)
    })
  })
})

describe('assertChatEmbedAllowed', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    flagState.isBillingEnabled = true
    flagState.isFreeApiDeploymentGateEnabled = true
    mockIsWorkspaceApiExecutionEntitled.mockResolvedValue(true)
    dbChainMockFns.limit.mockResolvedValue([{ workspaceId: 'ws-1' }])
  })

  it('returns 403 for a cross-site origin when the owner is on the free plan', async () => {
    mockIsWorkspaceApiExecutionEntitled.mockResolvedValueOnce(false)
    const res = await assertChatEmbedAllowed(
      chatRequest('https://evil.example.com'),
      'wf-1',
      'req-1'
    )
    expect(res?.status).toBe(403)
  })

  it('allows a cross-site origin when the owner is on a paid plan', async () => {
    const res = await assertChatEmbedAllowed(
      chatRequest('https://evil.example.com'),
      'wf-1',
      'req-1'
    )
    expect(res).toBeNull()
  })

  it('returns 403 for a cross-site origin when the workflow has no active workspace', async () => {
    dbChainMockFns.limit.mockResolvedValueOnce([])
    const res = await assertChatEmbedAllowed(
      chatRequest('https://evil.example.com'),
      'wf-1',
      'req-1'
    )
    expect(res?.status).toBe(403)
    expect(mockIsWorkspaceApiExecutionEntitled).not.toHaveBeenCalled()
  })

  it('allows a first-party *.sim.ai origin without gating', async () => {
    const res = await assertChatEmbedAllowed(chatRequest('https://chat.sim.ai'), 'wf-1', 'req-1')
    expect(res).toBeNull()
    expect(mockIsWorkspaceApiExecutionEntitled).not.toHaveBeenCalled()
  })

  it('allows requests with no Origin header', async () => {
    const res = await assertChatEmbedAllowed(chatRequest(), 'wf-1', 'req-1')
    expect(res).toBeNull()
    expect(mockIsWorkspaceApiExecutionEntitled).not.toHaveBeenCalled()
  })

  it('is a no-op when billing is disabled', async () => {
    flagState.isBillingEnabled = false
    const res = await assertChatEmbedAllowed(
      chatRequest('https://evil.example.com'),
      'wf-1',
      'req-1'
    )
    expect(res).toBeNull()
    expect(mockIsWorkspaceApiExecutionEntitled).not.toHaveBeenCalled()
  })

  it('is a no-op when the gate feature flag is disabled', async () => {
    flagState.isFreeApiDeploymentGateEnabled = false
    const res = await assertChatEmbedAllowed(
      chatRequest('https://evil.example.com'),
      'wf-1',
      'req-1'
    )
    expect(res).toBeNull()
    expect(mockIsWorkspaceApiExecutionEntitled).not.toHaveBeenCalled()
  })
})
