/**
 * Tests for chat OTP API route
 *
 * @vitest-environment node
 */
import {
  redisConfigMock,
  redisConfigMockFns,
  requestUtilsMockFns,
  workflowsApiUtilsMock,
  workflowsApiUtilsMockFns,
} from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

const {
  mockRedisSet,
  mockRedisGet,
  mockRedisDel,
  mockRedisTtl,
  mockRedisEval,
  mockRedisClient,
  mockDbSelect,
  mockDbInsert,
  mockDbDelete,
  mockDbUpdate,
  mockSendEmail,
  mockRenderOTPEmail,
  mockSetChatAuthCookie,
  mockGetStorageMethod,
  mockZodParse,
  mockGetEnv,
} = vi.hoisted(() => {
  const mockRedisSet = vi.fn()
  const mockRedisGet = vi.fn()
  const mockRedisDel = vi.fn()
  const mockRedisTtl = vi.fn()
  const mockRedisEval = vi.fn()
  const mockRedisClient = {
    set: mockRedisSet,
    get: mockRedisGet,
    del: mockRedisDel,
    ttl: mockRedisTtl,
    eval: mockRedisEval,
  }
  const mockDbSelect = vi.fn()
  const mockDbInsert = vi.fn()
  const mockDbDelete = vi.fn()
  const mockDbUpdate = vi.fn()
  const mockSendEmail = vi.fn()
  const mockRenderOTPEmail = vi.fn()
  const mockSetChatAuthCookie = vi.fn()
  const mockGetStorageMethod = vi.fn()
  const mockZodParse = vi.fn()
  const mockGetEnv = vi.fn()

  return {
    mockRedisSet,
    mockRedisGet,
    mockRedisDel,
    mockRedisTtl,
    mockRedisEval,
    mockRedisClient,
    mockDbSelect,
    mockDbInsert,
    mockDbDelete,
    mockDbUpdate,
    mockSendEmail,
    mockRenderOTPEmail,
    mockSetChatAuthCookie,
    mockGetStorageMethod,
    mockZodParse,
    mockGetEnv,
  }
})

const mockGetRedisClient = redisConfigMockFns.mockGetRedisClient
const mockCreateSuccessResponse = workflowsApiUtilsMockFns.mockCreateSuccessResponse
const mockCreateErrorResponse = workflowsApiUtilsMockFns.mockCreateErrorResponse

vi.mock('@/lib/core/config/redis', () => redisConfigMock)

vi.mock('@sim/db', () => ({
  db: {
    select: mockDbSelect,
    insert: mockDbInsert,
    delete: mockDbDelete,
    update: mockDbUpdate,
    transaction: vi.fn(async (callback: (tx: Record<string, unknown>) => unknown) => {
      return callback({
        select: mockDbSelect,
        insert: mockDbInsert,
        delete: mockDbDelete,
        update: mockDbUpdate,
      })
    }),
  },
}))

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

vi.mock('@/lib/core/storage', () => ({
  getStorageMethod: mockGetStorageMethod,
}))

const { mockCheckRateLimitDirect } = vi.hoisted(() => ({
  mockCheckRateLimitDirect: vi.fn(),
}))

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

vi.mock('@/lib/messaging/email/mailer', () => ({
  sendEmail: mockSendEmail,
}))

vi.mock('@/components/emails', () => ({
  renderOTPEmail: mockRenderOTPEmail,
}))

vi.mock('@/lib/core/security/deployment', () => ({
  isEmailAllowed: (email: string, allowedEmails: string[]) => {
    if (allowedEmails.includes(email)) return true
    const atIndex = email.indexOf('@')
    if (atIndex > 0) {
      const domain = email.substring(atIndex + 1)
      if (domain && allowedEmails.some((allowed: string) => allowed === `@${domain}`)) return true
    }
    return false
  },
}))

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

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

vi.mock('@/lib/core/config/env', () => ({
  env: {
    NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
    NODE_ENV: 'test',
  },
  getEnv: mockGetEnv,
  isTruthy: vi.fn().mockReturnValue(false),
  isFalsy: vi.fn().mockReturnValue(true),
}))

vi.mock('zod', () => {
  class ZodError extends Error {
    errors: Array<{ message: string }>
    constructor(issues: Array<{ message: string }>) {
      super('ZodError')
      this.errors = issues
    }
  }
  const chainable: Record<string, unknown> = {}
  const proxy: Record<string, unknown> = new Proxy(chainable, {
    get(target, prop) {
      if (prop === 'parse') return mockZodParse
      if (prop === 'safeParse') {
        return (data: unknown) => ({ success: true, data })
      }
      if (prop === 'then') return undefined
      if (typeof prop === 'symbol') return Reflect.get(target, prop)
      if (!(prop in target)) {
        target[prop as string] = vi.fn().mockReturnValue(proxy)
      }
      return target[prop as string]
    },
  })
  const makeChain = vi.fn(() => proxy)
  return {
    z: new Proxy(
      { ZodError },
      {
        get(target, prop) {
          if (prop === 'ZodError') return ZodError
          if (typeof prop === 'symbol') return Reflect.get(target, prop)
          return makeChain
        },
      }
    ),
  }
})

import { POST, PUT } from './route'

describe('Chat OTP API Route', () => {
  const mockEmail = 'test@example.com'
  const mockChatId = 'chat-123'
  const mockIdentifier = 'test-chat'
  const mockOTP = '123456'

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

    vi.spyOn(Math, 'random').mockReturnValue(0.123456)
    vi.spyOn(Date, 'now').mockReturnValue(1640995200000)

    vi.stubGlobal('crypto', {
      ...crypto,
      randomUUID: vi.fn().mockReturnValue('test-uuid-1234'),
    })

    mockGetRedisClient.mockReturnValue(mockRedisClient)
    mockRedisSet.mockResolvedValue('OK')
    mockRedisGet.mockResolvedValue(null)
    mockRedisDel.mockResolvedValue(1)
    mockRedisTtl.mockResolvedValue(600)

    const createDbChain = (result: unknown) => ({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          limit: vi.fn().mockResolvedValue(result),
        }),
      }),
    })

    mockDbSelect.mockImplementation(() => createDbChain([]))
    mockDbInsert.mockImplementation(() => ({
      values: vi.fn().mockResolvedValue(undefined),
    }))
    mockDbDelete.mockImplementation(() => ({
      where: vi.fn().mockResolvedValue(undefined),
    }))
    mockDbUpdate.mockImplementation(() => ({
      set: vi.fn().mockReturnValue({
        where: vi.fn().mockResolvedValue(undefined),
      }),
    }))

    mockGetStorageMethod.mockReturnValue('redis')

    mockSendEmail.mockResolvedValue({ success: true })
    mockRenderOTPEmail.mockResolvedValue('<html>OTP Email</html>')

    mockCreateSuccessResponse.mockImplementation((data: unknown) => ({
      json: () => Promise.resolve(data),
      status: 200,
    }))
    mockCreateErrorResponse.mockImplementation((message: string, status: number) => ({
      json: () => Promise.resolve({ error: message }),
      status,
    }))

    requestUtilsMockFns.mockGenerateRequestId.mockReturnValue('req-123')
    requestUtilsMockFns.mockGetClientIp.mockReturnValue('1.2.3.4')

    mockCheckRateLimitDirect.mockResolvedValue({
      allowed: true,
      remaining: 10,
      resetAt: new Date(Date.now() + 60_000),
    })

    mockZodParse.mockImplementation((data: unknown) => data)

    mockGetEnv.mockReturnValue('http://localhost:3000')
  })

  afterEach(() => {
    vi.restoreAllMocks()
  })

  describe('POST - Store OTP (Redis path)', () => {
    beforeEach(() => {
      mockGetStorageMethod.mockReturnValue('redis')
    })

    it('should store OTP in Redis when storage method is redis', async () => {
      mockDbSelect.mockImplementationOnce(() => ({
        from: vi.fn().mockReturnValue({
          where: vi.fn().mockReturnValue({
            limit: vi.fn().mockResolvedValue([
              {
                id: mockChatId,
                authType: 'email',
                allowedEmails: [mockEmail],
                title: 'Test Chat',
              },
            ]),
          }),
        }),
      }))

      const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
        method: 'POST',
        body: JSON.stringify({ email: mockEmail }),
      })

      await POST(request, { params: Promise.resolve({ identifier: mockIdentifier }) })

      expect(mockRedisSet).toHaveBeenCalledWith(
        `otp:${mockEmail}:${mockChatId}`,
        expect.any(String),
        'EX',
        900 // 15 minutes
      )

      expect(mockDbInsert).not.toHaveBeenCalled()
    })
  })

  describe('POST - Rate limiting', () => {
    const buildDeploymentSelect = () =>
      mockDbSelect.mockImplementationOnce(() => ({
        from: vi.fn().mockReturnValue({
          where: vi.fn().mockReturnValue({
            limit: vi.fn().mockResolvedValue([
              {
                id: mockChatId,
                authType: 'email',
                allowedEmails: [mockEmail],
                title: 'Test Chat',
              },
            ]),
          }),
        }),
      }))

    it('returns 429 with Retry-After when IP rate limit is exceeded', async () => {
      mockCheckRateLimitDirect.mockResolvedValueOnce({
        allowed: false,
        remaining: 0,
        resetAt: new Date(Date.now() + 900_000),
        retryAfterMs: 900_000,
      })

      const headerSet = vi.fn()
      mockCreateErrorResponse.mockImplementationOnce((message: string, status: number) => ({
        json: () => Promise.resolve({ error: message }),
        status,
        headers: { set: headerSet },
      }))

      const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
        method: 'POST',
        body: JSON.stringify({ email: mockEmail }),
      })

      const response = await POST(request, {
        params: Promise.resolve({ identifier: mockIdentifier }),
      })

      expect(response.status).toBe(429)
      expect(headerSet).toHaveBeenCalledWith('Retry-After', '900')
      expect(mockSendEmail).not.toHaveBeenCalled()
      expect(mockDbSelect).not.toHaveBeenCalled()
    })

    it('returns 429 with Retry-After when email rate limit is exceeded', async () => {
      mockCheckRateLimitDirect
        .mockResolvedValueOnce({
          allowed: true,
          remaining: 9,
          resetAt: new Date(Date.now() + 60_000),
        })
        .mockResolvedValueOnce({
          allowed: false,
          remaining: 0,
          resetAt: new Date(Date.now() + 900_000),
          retryAfterMs: 900_000,
        })

      const headerSet = vi.fn()
      mockCreateErrorResponse.mockImplementationOnce((message: string, status: number) => ({
        json: () => Promise.resolve({ error: message }),
        status,
        headers: { set: headerSet },
      }))

      buildDeploymentSelect()

      const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
        method: 'POST',
        body: JSON.stringify({ email: mockEmail }),
      })

      const response = await POST(request, {
        params: Promise.resolve({ identifier: mockIdentifier }),
      })

      expect(response.status).toBe(429)
      expect(headerSet).toHaveBeenCalledWith('Retry-After', '900')
      expect(mockSendEmail).not.toHaveBeenCalled()
    })

    it('falls back to refill interval when retryAfterMs is missing', async () => {
      mockCheckRateLimitDirect.mockResolvedValueOnce({
        allowed: false,
        remaining: 0,
        resetAt: new Date(Date.now() + 900_000),
      })

      const headerSet = vi.fn()
      mockCreateErrorResponse.mockImplementationOnce((message: string, status: number) => ({
        json: () => Promise.resolve({ error: message }),
        status,
        headers: { set: headerSet },
      }))

      const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
        method: 'POST',
        body: JSON.stringify({ email: mockEmail }),
      })

      await POST(request, { params: Promise.resolve({ identifier: mockIdentifier }) })

      expect(headerSet).toHaveBeenCalledWith('Retry-After', '900')
    })

    it('folds spoofed `unknown` client IPs into a single shared bucket', async () => {
      requestUtilsMockFns.mockGetClientIp.mockReturnValueOnce('unknown')
      buildDeploymentSelect()

      const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
        method: 'POST',
        body: JSON.stringify({ email: mockEmail }),
      })

      await POST(request, { params: Promise.resolve({ identifier: mockIdentifier }) })

      expect(mockCheckRateLimitDirect).toHaveBeenCalledTimes(2)
      expect(mockCheckRateLimitDirect).toHaveBeenCalledWith(
        expect.stringMatching(/^chat-otp:ip:.*:unknown$/),
        expect.any(Object)
      )
      expect(mockCheckRateLimitDirect).toHaveBeenCalledWith(
        expect.stringContaining('chat-otp:email:'),
        expect.any(Object)
      )
    })
  })

  describe('POST - Store OTP (Database path)', () => {
    beforeEach(() => {
      mockGetStorageMethod.mockReturnValue('database')
      mockGetRedisClient.mockReturnValue(null)
    })

    it('should store OTP in database when storage method is database', async () => {
      mockDbSelect.mockImplementationOnce(() => ({
        from: vi.fn().mockReturnValue({
          where: vi.fn().mockReturnValue({
            limit: vi.fn().mockResolvedValue([
              {
                id: mockChatId,
                authType: 'email',
                allowedEmails: [mockEmail],
                title: 'Test Chat',
              },
            ]),
          }),
        }),
      }))

      const mockInsertValues = vi.fn().mockResolvedValue(undefined)
      mockDbInsert.mockImplementationOnce(() => ({
        values: mockInsertValues,
      }))

      const mockDeleteWhere = vi.fn().mockResolvedValue(undefined)
      mockDbDelete.mockImplementation(() => ({
        where: mockDeleteWhere,
      }))

      const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
        method: 'POST',
        body: JSON.stringify({ email: mockEmail }),
      })

      await POST(request, { params: Promise.resolve({ identifier: mockIdentifier }) })

      expect(mockDbDelete).toHaveBeenCalled()

      expect(mockDbInsert).toHaveBeenCalled()
      expect(mockInsertValues).toHaveBeenCalledWith({
        id: expect.any(String),
        identifier: `chat-otp:${mockChatId}:${mockEmail}`,
        value: expect.any(String),
        expiresAt: expect.any(Date),
        createdAt: expect.any(Date),
        updatedAt: expect.any(Date),
      })

      expect(mockRedisSet).not.toHaveBeenCalled()
    })
  })

  describe('PUT - Verify OTP (Redis path)', () => {
    beforeEach(() => {
      mockGetStorageMethod.mockReturnValue('redis')
      mockRedisGet.mockResolvedValue(`${mockOTP}:0`)
    })

    it('should retrieve OTP from Redis and verify successfully', async () => {
      mockDbSelect.mockImplementationOnce(() => ({
        from: vi.fn().mockReturnValue({
          where: vi.fn().mockReturnValue({
            limit: vi.fn().mockResolvedValue([
              {
                id: mockChatId,
                authType: 'email',
              },
            ]),
          }),
        }),
      }))

      const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
        method: 'PUT',
        body: JSON.stringify({ email: mockEmail, otp: mockOTP }),
      })

      await PUT(request, { params: Promise.resolve({ identifier: mockIdentifier }) })

      expect(mockRedisGet).toHaveBeenCalledWith(`otp:${mockEmail}:${mockChatId}`)
      expect(mockRedisDel).toHaveBeenCalledWith(`otp:${mockEmail}:${mockChatId}`)
      expect(mockDbSelect).toHaveBeenCalledTimes(1)
    })
  })

  describe('PUT - Verify OTP (authType re-check)', () => {
    beforeEach(() => {
      mockGetStorageMethod.mockReturnValue('redis')
      mockRedisGet.mockResolvedValue(`${mockOTP}:0`)
    })

    it('rejects verification when the chat has switched away from email auth', async () => {
      mockDbSelect.mockImplementationOnce(() => ({
        from: vi.fn().mockReturnValue({
          where: vi.fn().mockReturnValue({
            limit: vi.fn().mockResolvedValue([
              {
                id: mockChatId,
                authType: 'password',
                password: 'encrypted-password',
              },
            ]),
          }),
        }),
      }))

      const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
        method: 'PUT',
        body: JSON.stringify({ email: mockEmail, otp: mockOTP }),
      })

      await PUT(request, { params: Promise.resolve({ identifier: mockIdentifier }) })

      expect(mockCreateErrorResponse).toHaveBeenCalledWith(
        'This chat does not use email authentication',
        400
      )
      expect(mockRedisGet).not.toHaveBeenCalled()
      expect(mockSetChatAuthCookie).not.toHaveBeenCalled()
    })
  })

  describe('PUT - Verify OTP (Database path)', () => {
    beforeEach(() => {
      mockGetStorageMethod.mockReturnValue('database')
      mockGetRedisClient.mockReturnValue(null)
    })

    it('should retrieve OTP from database and verify successfully', async () => {
      let selectCallCount = 0

      mockDbSelect.mockImplementation(() => ({
        from: vi.fn().mockReturnValue({
          where: vi.fn().mockReturnValue({
            limit: vi.fn().mockImplementation(() => {
              selectCallCount++
              if (selectCallCount === 1) {
                return Promise.resolve([
                  {
                    id: mockChatId,
                    authType: 'email',
                  },
                ])
              }
              return Promise.resolve([
                {
                  value: `${mockOTP}:0`,
                  expiresAt: new Date(Date.now() + 10 * 60 * 1000),
                },
              ])
            }),
          }),
        }),
      }))

      const mockDeleteWhere = vi.fn().mockResolvedValue(undefined)
      mockDbDelete.mockImplementation(() => ({
        where: mockDeleteWhere,
      }))

      const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
        method: 'PUT',
        body: JSON.stringify({ email: mockEmail, otp: mockOTP }),
      })

      await PUT(request, { params: Promise.resolve({ identifier: mockIdentifier }) })

      expect(mockDbSelect).toHaveBeenCalledTimes(2)

      expect(mockDbDelete).toHaveBeenCalled()

      expect(mockRedisGet).not.toHaveBeenCalled()
    })

    it('should reject expired OTP from database', async () => {
      let selectCallCount = 0

      mockDbSelect.mockImplementation(() => ({
        from: vi.fn().mockReturnValue({
          where: vi.fn().mockReturnValue({
            limit: vi.fn().mockImplementation(() => {
              selectCallCount++
              if (selectCallCount === 1) {
                return Promise.resolve([
                  {
                    id: mockChatId,
                    authType: 'email',
                  },
                ])
              }
              return Promise.resolve([])
            }),
          }),
        }),
      }))

      const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
        method: 'PUT',
        body: JSON.stringify({ email: mockEmail, otp: mockOTP }),
      })

      await PUT(request, { params: Promise.resolve({ identifier: mockIdentifier }) })

      expect(mockCreateErrorResponse).toHaveBeenCalledWith(
        'No verification code found, request a new one',
        400
      )
    })
  })

  describe('DELETE OTP (Redis path)', () => {
    beforeEach(() => {
      mockGetStorageMethod.mockReturnValue('redis')
    })

    it('should delete OTP from Redis after verification', async () => {
      mockRedisGet.mockResolvedValue(`${mockOTP}:0`)

      mockDbSelect.mockImplementationOnce(() => ({
        from: vi.fn().mockReturnValue({
          where: vi.fn().mockReturnValue({
            limit: vi.fn().mockResolvedValue([
              {
                id: mockChatId,
                authType: 'email',
              },
            ]),
          }),
        }),
      }))

      const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
        method: 'PUT',
        body: JSON.stringify({ email: mockEmail, otp: mockOTP }),
      })

      await PUT(request, { params: Promise.resolve({ identifier: mockIdentifier }) })

      expect(mockRedisDel).toHaveBeenCalledWith(`otp:${mockEmail}:${mockChatId}`)
      expect(mockDbDelete).not.toHaveBeenCalled()
    })
  })

  describe('DELETE OTP (Database path)', () => {
    beforeEach(() => {
      mockGetStorageMethod.mockReturnValue('database')
      mockGetRedisClient.mockReturnValue(null)
    })

    it('should delete OTP from database after verification', async () => {
      let selectCallCount = 0
      mockDbSelect.mockImplementation(() => ({
        from: vi.fn().mockReturnValue({
          where: vi.fn().mockReturnValue({
            limit: vi.fn().mockImplementation(() => {
              selectCallCount++
              if (selectCallCount === 1) {
                return Promise.resolve([{ id: mockChatId, authType: 'email' }])
              }
              return Promise.resolve([
                { value: `${mockOTP}:0`, expiresAt: new Date(Date.now() + 10 * 60 * 1000) },
              ])
            }),
          }),
        }),
      }))

      const mockDeleteWhere = vi.fn().mockResolvedValue(undefined)
      mockDbDelete.mockImplementation(() => ({
        where: mockDeleteWhere,
      }))

      const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
        method: 'PUT',
        body: JSON.stringify({ email: mockEmail, otp: mockOTP }),
      })

      await PUT(request, { params: Promise.resolve({ identifier: mockIdentifier }) })

      expect(mockDbDelete).toHaveBeenCalled()
      expect(mockRedisDel).not.toHaveBeenCalled()
    })
  })

  describe('Brute-force protection', () => {
    beforeEach(() => {
      mockGetStorageMethod.mockReturnValue('redis')
    })

    it('should atomically increment attempts on wrong OTP', async () => {
      mockRedisGet.mockResolvedValue('654321:0')
      mockRedisEval.mockResolvedValue('654321:1')

      mockDbSelect.mockImplementationOnce(() => ({
        from: vi.fn().mockReturnValue({
          where: vi.fn().mockReturnValue({
            limit: vi.fn().mockResolvedValue([{ id: mockChatId, authType: 'email' }]),
          }),
        }),
      }))

      const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
        method: 'PUT',
        body: JSON.stringify({ email: mockEmail, otp: 'wrong1' }),
      })

      await PUT(request, { params: Promise.resolve({ identifier: mockIdentifier }) })

      expect(mockRedisEval).toHaveBeenCalledWith(
        expect.any(String),
        1,
        `otp:${mockEmail}:${mockChatId}`,
        5
      )
      expect(mockCreateErrorResponse).toHaveBeenCalledWith('Invalid verification code', 400)
    })

    it('should invalidate OTP and return 429 after max failed attempts', async () => {
      mockRedisGet.mockResolvedValue('654321:4')
      mockRedisEval.mockResolvedValue('LOCKED')

      mockDbSelect.mockImplementationOnce(() => ({
        from: vi.fn().mockReturnValue({
          where: vi.fn().mockReturnValue({
            limit: vi.fn().mockResolvedValue([{ id: mockChatId, authType: 'email' }]),
          }),
        }),
      }))

      const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
        method: 'PUT',
        body: JSON.stringify({ email: mockEmail, otp: 'wrong5' }),
      })

      await PUT(request, { params: Promise.resolve({ identifier: mockIdentifier }) })

      expect(mockRedisEval).toHaveBeenCalled()
      expect(mockCreateErrorResponse).toHaveBeenCalledWith(
        'Too many failed attempts. Please request a new code.',
        429
      )
    })

    it('should store OTP with zero attempts on generation', async () => {
      mockDbSelect.mockImplementationOnce(() => ({
        from: vi.fn().mockReturnValue({
          where: vi.fn().mockReturnValue({
            limit: vi.fn().mockResolvedValue([
              {
                id: mockChatId,
                authType: 'email',
                allowedEmails: [mockEmail],
                title: 'Test Chat',
              },
            ]),
          }),
        }),
      }))

      const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
        method: 'POST',
        body: JSON.stringify({ email: mockEmail }),
      })

      await POST(request, { params: Promise.resolve({ identifier: mockIdentifier }) })

      expect(mockRedisSet).toHaveBeenCalledWith(
        `otp:${mockEmail}:${mockChatId}`,
        expect.stringMatching(/^\d{6}:0$/),
        'EX',
        900
      )
    })
  })

  describe('Behavior consistency between Redis and Database', () => {
    it('should have same behavior for missing OTP in both storage methods', async () => {
      mockGetStorageMethod.mockReturnValue('redis')
      mockRedisGet.mockResolvedValue(null)

      mockDbSelect.mockImplementation(() => ({
        from: vi.fn().mockReturnValue({
          where: vi.fn().mockReturnValue({
            limit: vi.fn().mockResolvedValue([{ id: mockChatId, authType: 'email' }]),
          }),
        }),
      }))

      const requestRedis = new NextRequest('http://localhost:3000/api/chat/test/otp', {
        method: 'PUT',
        body: JSON.stringify({ email: mockEmail, otp: mockOTP }),
      })

      await PUT(requestRedis, { params: Promise.resolve({ identifier: mockIdentifier }) })

      expect(mockCreateErrorResponse).toHaveBeenCalledWith(
        'No verification code found, request a new one',
        400
      )
    })

    it('should have same OTP expiry time in both storage methods', async () => {
      const OTP_EXPIRY = 15 * 60

      mockGetStorageMethod.mockReturnValue('redis')

      mockDbSelect.mockImplementation(() => ({
        from: vi.fn().mockReturnValue({
          where: vi.fn().mockReturnValue({
            limit: vi.fn().mockResolvedValue([
              {
                id: mockChatId,
                authType: 'email',
                allowedEmails: [mockEmail],
                title: 'Test Chat',
              },
            ]),
          }),
        }),
      }))

      const requestRedis = new NextRequest('http://localhost:3000/api/chat/test/otp', {
        method: 'POST',
        body: JSON.stringify({ email: mockEmail }),
      })

      await POST(requestRedis, { params: Promise.resolve({ identifier: mockIdentifier }) })

      expect(mockRedisSet).toHaveBeenCalledWith(
        expect.any(String),
        expect.any(String),
        'EX',
        OTP_EXPIRY
      )
    })
  })
})
