/**
 * Tests for chat identifier API route
 *
 * @vitest-environment node
 */
import {
  dbChainMock,
  dbChainMockFns,
  encryptionMock,
  executionPreprocessingMock,
  executionPreprocessingMockFns,
  loggingSessionMock,
  workflowsApiUtilsMock,
  workflowsApiUtilsMockFns,
} from '@sim/testing'
import { NextResponse } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'

/**
 * Creates a mock NextRequest with cookies support for testing.
 */
function createMockNextRequest(
  method = 'GET',
  body?: unknown,
  headers: Record<string, string> = {},
  url = 'http://localhost:3000/api/test'
): any {
  const headersObj = new Headers({
    'Content-Type': 'application/json',
    ...headers,
  })

  const parsedUrl = new URL(url)

  return {
    method,
    headers: headersObj,
    nextUrl: parsedUrl,
    cookies: {
      get: vi.fn().mockReturnValue(undefined),
    },
    json:
      body !== undefined
        ? vi.fn().mockResolvedValue(body)
        : vi.fn().mockRejectedValue(new Error('No body')),
    url,
  }
}

const createMockStream = () => {
  return new ReadableStream({
    start(controller) {
      controller.enqueue(
        new TextEncoder().encode('data: {"blockId":"agent-1","chunk":"Hello"}\n\n')
      )
      controller.enqueue(
        new TextEncoder().encode('data: {"blockId":"agent-1","chunk":" world"}\n\n')
      )
      controller.enqueue(
        new TextEncoder().encode('data: {"event":"final","data":{"success":true}}\n\n')
      )
      controller.close()
    },
  })
}

const {
  mockValidateChatAuth,
  mockSetChatAuthCookie,
  mockValidateAuthToken,
  mockAssertChatEmbedAllowed,
} = vi.hoisted(() => ({
  mockValidateChatAuth: vi.fn().mockResolvedValue({ authorized: true }),
  mockSetChatAuthCookie: vi.fn(),
  mockValidateAuthToken: vi.fn().mockReturnValue(false),
  mockAssertChatEmbedAllowed: vi.fn().mockResolvedValue(null),
}))

const mockCreateErrorResponse = workflowsApiUtilsMockFns.mockCreateErrorResponse
const mockCreateSuccessResponse = workflowsApiUtilsMockFns.mockCreateSuccessResponse

vi.mock('@sim/db', () => ({
  ...dbChainMock,
  chat: {},
  workflow: {},
}))

vi.mock('@/lib/core/security/deployment', () => ({
  validateAuthToken: mockValidateAuthToken,
  setDeploymentAuthCookie: vi.fn(),
  isEmailAllowed: vi.fn().mockReturnValue(false),
}))

vi.mock('@/app/api/chat/utils', () => ({
  validateChatAuth: mockValidateChatAuth,
  setChatAuthCookie: mockSetChatAuthCookie,
  assertChatEmbedAllowed: mockAssertChatEmbedAllowed,
}))

vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock)

vi.mock('@/lib/execution/preprocessing', () => executionPreprocessingMock)

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

vi.mock('@/lib/workflows/streaming/streaming', () => ({
  createStreamingResponse: vi.fn().mockImplementation(async () => createMockStream()),
}))

vi.mock('@/lib/workflows/executor/execute-workflow', () => ({
  executeWorkflow: vi.fn().mockResolvedValue({ success: true, output: {} }),
}))

vi.mock('@/lib/core/utils/sse', () => ({
  SSE_HEADERS: {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
    'X-Accel-Buffering': 'no',
  },
}))

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

import { preprocessExecution } from '@/lib/execution/preprocessing'
import { createStreamingResponse } from '@/lib/workflows/streaming/streaming'
import { GET, POST } from '@/app/api/chat/[identifier]/route'

describe('Chat Identifier API Route', () => {
  const mockChatResult = [
    {
      id: 'chat-id',
      workflowId: 'workflow-id',
      userId: 'user-id',
      isActive: true,
      authType: 'public',
      title: 'Test Chat',
      description: 'Test chat description',
      customizations: {
        welcomeMessage: 'Welcome to the test chat',
        primaryColor: '#000000',
      },
      outputConfigs: [{ blockId: 'block-1', path: 'output' }],
    },
  ]

  const mockWorkflowResult = [
    {
      isDeployed: true,
      state: {
        blocks: {},
        edges: [],
        loops: {},
        parallels: {},
      },
      deployedState: {
        blocks: {},
        edges: [],
        loops: {},
        parallels: {},
      },
    },
  ]

  beforeEach(() => {
    vi.clearAllMocks()

    executionPreprocessingMockFns.mockPreprocessExecution.mockResolvedValue({
      success: true,
      actorUserId: 'test-user-id',
      workflowRecord: {
        id: 'test-workflow-id',
        userId: 'test-user-id',
        isDeployed: true,
        workspaceId: 'test-workspace-id',
        variables: {},
      },
      userSubscription: {
        plan: 'pro',
        status: 'active',
      },
      rateLimitInfo: {
        allowed: true,
        remaining: 100,
        resetAt: new Date(),
      },
    })

    mockValidateChatAuth.mockResolvedValue({ authorized: true })
    mockValidateAuthToken.mockReturnValue(false)
    mockCreateErrorResponse.mockImplementation((message: string, status: number, code?: string) => {
      return new Response(
        JSON.stringify({
          error: code || 'Error',
          message,
        }),
        { status }
      )
    })
    mockCreateSuccessResponse.mockImplementation((data: unknown) => {
      return new Response(JSON.stringify(data), { status: 200 })
    })

    dbChainMockFns.select.mockImplementation((fields: Record<string, unknown>) => {
      if (fields && fields.isDeployed !== undefined) {
        return {
          from: vi.fn().mockReturnValue({
            where: vi.fn().mockReturnValue({
              limit: vi.fn().mockReturnValue(mockWorkflowResult),
            }),
          }),
        }
      }
      return {
        from: vi.fn().mockReturnValue({
          where: vi.fn().mockReturnValue({
            limit: vi.fn().mockReturnValue(mockChatResult),
          }),
        }),
      }
    })
  })

  describe('GET endpoint', () => {
    it('should return chat info for a valid identifier', async () => {
      const req = createMockNextRequest('GET')
      const params = Promise.resolve({ identifier: 'test-chat' })

      const response = await GET(req, { params })

      expect(response.status).toBe(200)

      const data = await response.json()
      expect(data).toHaveProperty('id', 'chat-id')
      expect(data).toHaveProperty('title', 'Test Chat')
      expect(data).toHaveProperty('description', 'Test chat description')
      expect(data).toHaveProperty('customizations')
      expect(data.customizations).toHaveProperty('welcomeMessage', 'Welcome to the test chat')
    })

    it('should return 403 when embedding is blocked for a cross-origin caller', async () => {
      mockAssertChatEmbedAllowed.mockResolvedValueOnce(
        NextResponse.json(
          { error: 'Embedding this chat on external sites requires a paid plan' },
          {
            status: 403,
          }
        )
      )

      const req = createMockNextRequest('GET', undefined, { origin: 'https://evil.example.com' })
      const params = Promise.resolve({ identifier: 'test-chat' })

      const response = await GET(req, { params })

      expect(response.status).toBe(403)
    })

    it('should return 404 for non-existent identifier', async () => {
      dbChainMockFns.select.mockImplementation(() => {
        return {
          from: vi.fn().mockReturnValue({
            where: vi.fn().mockReturnValue({
              limit: vi.fn().mockReturnValue([]),
            }),
          }),
        }
      })

      const req = createMockNextRequest('GET')
      const params = Promise.resolve({ identifier: 'nonexistent' })

      const response = await GET(req, { params })

      expect(response.status).toBe(404)

      const data = await response.json()
      expect(data).toHaveProperty('error')
      expect(data).toHaveProperty('message', 'Chat not found')
    })

    it('should return 403 for inactive chat', async () => {
      dbChainMockFns.select.mockImplementation(() => {
        return {
          from: vi.fn().mockReturnValue({
            where: vi.fn().mockReturnValue({
              limit: vi.fn().mockReturnValue([
                {
                  id: 'chat-id',
                  isActive: false,
                  authType: 'public',
                },
              ]),
            }),
          }),
        }
      })

      const req = createMockNextRequest('GET')
      const params = Promise.resolve({ identifier: 'inactive-chat' })

      const response = await GET(req, { params })

      expect(response.status).toBe(403)

      const data = await response.json()
      expect(data).toHaveProperty('error')
      expect(data).toHaveProperty('message', 'This chat is currently unavailable')
    })

    it('should return 401 when authentication is required', async () => {
      mockValidateChatAuth.mockResolvedValueOnce({
        authorized: false,
        error: 'auth_required_password',
      })

      const req = createMockNextRequest('GET')
      const params = Promise.resolve({ identifier: 'password-protected-chat' })

      const response = await GET(req, { params })

      expect(response.status).toBe(401)

      const data = await response.json()
      expect(data).toHaveProperty('error')
      expect(data).toHaveProperty('message', 'auth_required_password')
    })
  })

  describe('POST endpoint', () => {
    it('should return 403 when embedding is blocked for a cross-origin caller', async () => {
      mockAssertChatEmbedAllowed.mockResolvedValueOnce(
        NextResponse.json(
          { error: 'Embedding this chat on external sites requires a paid plan' },
          {
            status: 403,
          }
        )
      )

      const req = createMockNextRequest(
        'POST',
        { input: 'Hello' },
        { origin: 'https://evil.example.com' }
      )
      const params = Promise.resolve({ identifier: 'test-chat' })

      const response = await POST(req, { params })

      expect(response.status).toBe(403)
    })

    it('should return chat config on successful authentication', async () => {
      const req = createMockNextRequest('POST', { password: 'test-password' })
      const params = Promise.resolve({ identifier: 'password-protected-chat' })

      const response = await POST(req, { params })

      expect(response.status).toBe(200)

      const data = await response.json()
      expect(data).toHaveProperty('id', 'chat-id')
      expect(data).toHaveProperty('title', 'Test Chat')
      expect(data).toHaveProperty('customizations')
      expect(data.customizations).toHaveProperty('welcomeMessage', 'Welcome to the test chat')

      expect(mockSetChatAuthCookie).toHaveBeenCalled()
    })

    it('should return 400 for requests without input', async () => {
      const req = createMockNextRequest('POST', {})
      const params = Promise.resolve({ identifier: 'test-chat' })

      const response = await POST(req, { params })

      expect(response.status).toBe(400)

      const data = await response.json()
      expect(data).toHaveProperty('error')
      expect(data).toHaveProperty('message', 'No input provided')
    })

    it('should return 401 for unauthorized access', async () => {
      mockValidateChatAuth.mockResolvedValueOnce({
        authorized: false,
        error: 'Authentication required',
      })

      const req = createMockNextRequest('POST', { input: 'Hello' })
      const params = Promise.resolve({ identifier: 'protected-chat' })

      const response = await POST(req, { params })

      expect(response.status).toBe(401)

      const data = await response.json()
      expect(data).toHaveProperty('error')
      expect(data).toHaveProperty('message', 'Authentication required')
    })

    it('should return 503 when workflow is not available', async () => {
      vi.mocked(preprocessExecution).mockResolvedValueOnce({
        success: false,
        error: {
          message: 'Workflow is not deployed',
          statusCode: 403,
          logCreated: false,
        },
      })

      const req = createMockNextRequest('POST', { input: 'Hello' })
      const params = Promise.resolve({ identifier: 'test-chat' })

      const response = await POST(req, { params })

      expect(response.status).toBe(403)

      const data = await response.json()
      expect(data).toHaveProperty('error')
      expect(data).toHaveProperty('message', 'Workflow is not deployed')
    })

    it('should return streaming response for valid chat messages', async () => {
      const req = createMockNextRequest('POST', {
        input: 'Hello world',
        conversationId: 'conv-123',
      })
      const params = Promise.resolve({ identifier: 'test-chat' })

      const response = await POST(req, { params })

      expect(response.status).toBe(200)
      expect(response.headers.get('Content-Type')).toBe('text/event-stream')
      expect(response.headers.get('Cache-Control')).toBe('no-cache')
      expect(response.headers.get('Connection')).toBe('keep-alive')

      expect(createStreamingResponse).toHaveBeenCalledWith(
        expect.objectContaining({
          executeFn: expect.any(Function),
          streamConfig: expect.objectContaining({
            isSecureMode: true,
            workflowTriggerType: 'chat',
          }),
        })
      )
    }, 10000)

    it('should handle streaming response body correctly', async () => {
      const req = createMockNextRequest('POST', { input: 'Hello world' })
      const params = Promise.resolve({ identifier: 'test-chat' })

      const response = await POST(req, { params })

      expect(response.status).toBe(200)
      expect(response.body).toBeInstanceOf(ReadableStream)

      if (response.body) {
        const reader = response.body.getReader()
        const { value, done } = await reader.read()

        if (!done && value) {
          const chunk = new TextDecoder().decode(value)
          expect(chunk).toMatch(/^data: /)
        }

        reader.releaseLock()
      }
    })

    it('should handle workflow execution errors gracefully', async () => {
      vi.mocked(createStreamingResponse).mockImplementationOnce(async () => {
        throw new Error('Execution failed')
      })

      const req = createMockNextRequest('POST', { input: 'Trigger error' })
      const params = Promise.resolve({ identifier: 'test-chat' })

      const response = await POST(req, { params })

      expect(response.status).toBe(500)

      const data = await response.json()
      expect(data).toHaveProperty('error')
      expect(data).toHaveProperty('message', 'Execution failed')
    })

    it('should handle invalid JSON in request body', async () => {
      const req = {
        method: 'POST',
        headers: new Headers(),
        nextUrl: new URL('http://localhost:3000/api/test'),
        cookies: { get: vi.fn().mockReturnValue(undefined) },
        json: vi.fn().mockRejectedValue(new Error('Invalid JSON')),
      } as any

      const params = Promise.resolve({ identifier: 'test-chat' })

      const response = await POST(req, { params })

      expect(response.status).toBe(400)

      const data = await response.json()
      expect(data).toHaveProperty('error')
      expect(data).toHaveProperty('message', 'Invalid request body')
    })

    it('should pass conversationId to streaming execution when provided', async () => {
      const req = createMockNextRequest('POST', {
        input: 'Hello world',
        conversationId: 'test-conversation-123',
      })
      const params = Promise.resolve({ identifier: 'test-chat' })

      await POST(req, { params })

      expect(createStreamingResponse).toHaveBeenCalledWith(
        expect.objectContaining({
          executeFn: expect.any(Function),
          streamConfig: expect.objectContaining({
            workflowTriggerType: 'chat',
          }),
        })
      )
    })

    it('should handle missing conversationId gracefully', async () => {
      const req = createMockNextRequest('POST', { input: 'Hello world' })
      const params = Promise.resolve({ identifier: 'test-chat' })

      await POST(req, { params })

      expect(createStreamingResponse).toHaveBeenCalledWith(
        expect.objectContaining({
          executeFn: expect.any(Function),
        })
      )
    })
  })
})
