import { describe, it, expect, beforeEach, vi } from 'vitest'
import { NextRequest } from 'next/server'

// Mock the DB pool before the route is imported
const queryMock = vi.fn()
vi.mock('@/lib/db', () => ({
  default: { query: (...args: unknown[]) => queryMock(...args) },
}))

// Mock the email sender so no SMTP is attempted
const sendMagicLinkEmailMock = vi.fn().mockResolvedValue(undefined)
vi.mock('@/lib/email', () => ({
  sendMagicLinkEmail: (...args: unknown[]) => sendMagicLinkEmailMock(...args),
}))

// Import after mocks are registered
import { POST } from '@/app/api/auth/request-link/route'

// Unique suffix per test so the rate-limit store (keyed by IP / email) doesn't
// bleed across cases — the rate-limiter holds state at module scope.
let testId = 0
function uniqueIp() {
  testId++
  return `10.0.${Math.floor(testId / 250)}.${testId % 250}`
}

function makeRequest(body: unknown, ip = uniqueIp()): NextRequest {
  return new NextRequest('http://localhost/api/auth/request-link', {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'x-forwarded-for': ip,
    },
    body: JSON.stringify(body),
  })
}

function investorRow(overrides: Partial<{ id: string; email: string; name: string | null; status: string }> = {}) {
  return {
    id: overrides.id ?? 'investor-1',
    email: overrides.email ?? 'invited@example.com',
    name: overrides.name ?? 'Alice',
    status: overrides.status ?? 'invited',
  }
}

beforeEach(() => {
  queryMock.mockReset()
  sendMagicLinkEmailMock.mockReset()
  sendMagicLinkEmailMock.mockResolvedValue(undefined)
})

describe('POST /api/auth/request-link — input validation', () => {
  it('returns 400 when email is missing', async () => {
    const res = await POST(makeRequest({}))
    expect(res.status).toBe(400)
    const body = await res.json()
    expect(body.error).toBe('Email required')
    expect(queryMock).not.toHaveBeenCalled()
    expect(sendMagicLinkEmailMock).not.toHaveBeenCalled()
  })

  it('returns 400 when email is not a string', async () => {
    const res = await POST(makeRequest({ email: 12345 }))
    expect(res.status).toBe(400)
    expect(sendMagicLinkEmailMock).not.toHaveBeenCalled()
  })

  it('handles malformed JSON body as missing email (400)', async () => {
    const req = new NextRequest('http://localhost/api/auth/request-link', {
      method: 'POST',
      headers: { 'content-type': 'application/json', 'x-forwarded-for': uniqueIp() },
      body: 'not-json',
    })
    const res = await POST(req)
    expect(res.status).toBe(400)
  })
})

describe('POST /api/auth/request-link — unknown email (enumeration resistance)', () => {
  it('returns the generic success response without sending email', async () => {
    // First query: investor lookup → empty rows
    queryMock.mockResolvedValueOnce({ rows: [] })
    // Second query: the audit log insert
    queryMock.mockResolvedValueOnce({ rows: [] })

    const res = await POST(makeRequest({ email: 'unknown@example.com' }))
    expect(res.status).toBe(200)
    const body = await res.json()
    expect(body.success).toBe(true)
    expect(body.message).toMatch(/if this email was invited/i)
    expect(sendMagicLinkEmailMock).not.toHaveBeenCalled()

    // Verify the investor-lookup SQL was issued with the normalized email
    const [sql, params] = queryMock.mock.calls[0]
    expect(sql).toMatch(/FROM pitch_investors WHERE email/i)
    expect(params).toEqual(['unknown@example.com'])
  })

  it('normalizes email (trim + lowercase) before lookup', async () => {
    queryMock.mockResolvedValueOnce({ rows: [] })
    queryMock.mockResolvedValueOnce({ rows: [] })

    await POST(makeRequest({ email: '  Mixed@Example.COM  ' }))

    const [, params] = queryMock.mock.calls[0]
    expect(params).toEqual(['mixed@example.com'])
  })
})

describe('POST /api/auth/request-link — known investor', () => {
  it('creates a new magic link and sends the email with generic response', async () => {
    // 1st: investor lookup → found
    queryMock.mockResolvedValueOnce({ rows: [investorRow()] })
    // 2nd: magic link insert
    queryMock.mockResolvedValueOnce({ rows: [] })
    // 3rd: audit log insert
    queryMock.mockResolvedValueOnce({ rows: [] })

    const res = await POST(makeRequest({ email: 'invited@example.com' }))
    expect(res.status).toBe(200)
    const body = await res.json()
    expect(body.success).toBe(true)
    // Response is identical to the unknown-email case (no information leak)
    expect(body.message).toMatch(/if this email was invited/i)

    // Verify magic link insert
    const [insertSql, insertParams] = queryMock.mock.calls[1]
    expect(insertSql).toMatch(/INSERT INTO pitch_magic_links/i)
    expect(insertParams[0]).toBe('investor-1')
    expect(insertParams[1]).toMatch(/^[0-9a-f]{96}$/) // 96-char hex token
    expect(insertParams[2]).toBeInstanceOf(Date)

    // Verify email was sent with the fresh token URL
    expect(sendMagicLinkEmailMock).toHaveBeenCalledTimes(1)
    const [emailTo, emailName, magicLinkUrl] = sendMagicLinkEmailMock.mock.calls[0]
    expect(emailTo).toBe('invited@example.com')
    expect(emailName).toBe('Alice')
    expect(magicLinkUrl).toMatch(/\/auth\/verify\?token=[0-9a-f]{96}$/)
  })

  it('generates a different token on each call (re-invite is always fresh)', async () => {
    // Call 1
    queryMock.mockResolvedValueOnce({ rows: [investorRow({ email: 'a@x.com' })] })
    queryMock.mockResolvedValueOnce({ rows: [] })
    queryMock.mockResolvedValueOnce({ rows: [] })
    await POST(makeRequest({ email: 'a@x.com' }))

    // Call 2 — different email to avoid the per-email rate limit
    queryMock.mockResolvedValueOnce({ rows: [investorRow({ email: 'b@x.com' })] })
    queryMock.mockResolvedValueOnce({ rows: [] })
    queryMock.mockResolvedValueOnce({ rows: [] })
    await POST(makeRequest({ email: 'b@x.com' }))

    const token1 = queryMock.mock.calls[1][1][1]
    const token2 = queryMock.mock.calls[4][1][1]
    expect(token1).not.toBe(token2)
  })

  it('skips email send for a revoked investor (returns generic response)', async () => {
    queryMock.mockResolvedValueOnce({ rows: [investorRow({ status: 'revoked' })] })
    queryMock.mockResolvedValueOnce({ rows: [] }) // audit log

    const res = await POST(makeRequest({ email: 'invited@example.com' }))
    expect(res.status).toBe(200)
    const body = await res.json()
    expect(body.success).toBe(true)
    expect(sendMagicLinkEmailMock).not.toHaveBeenCalled()

    // Ensure no magic link was inserted
    const inserts = queryMock.mock.calls.filter(c => /INSERT INTO pitch_magic_links/i.test(c[0]))
    expect(inserts.length).toBe(0)
  })
})

describe('POST /api/auth/request-link — rate limiting', () => {
  it('throttles after N requests per email and returns generic success (silent throttle)', async () => {
    const email = `throttle-${Date.now()}@example.com`

    // First 3 requests succeed (RATE_LIMITS.magicLink.limit = 3)
    for (let i = 0; i < 3; i++) {
      queryMock.mockResolvedValueOnce({ rows: [investorRow({ email })] })
      queryMock.mockResolvedValueOnce({ rows: [] }) // magic link insert
      queryMock.mockResolvedValueOnce({ rows: [] }) // audit log
      const res = await POST(makeRequest({ email }))
      expect(res.status).toBe(200)
    }
    expect(sendMagicLinkEmailMock).toHaveBeenCalledTimes(3)

    // 4th request is silently throttled — same generic response, no email sent
    queryMock.mockResolvedValueOnce({ rows: [] }) // audit log only
    const res4 = await POST(makeRequest({ email }))
    expect(res4.status).toBe(200)
    const body4 = await res4.json()
    expect(body4.success).toBe(true)
    // Still exactly 3 emails sent — nothing new
    expect(sendMagicLinkEmailMock).toHaveBeenCalledTimes(3)
  })

  it('throttles with 429 after too many attempts from the same IP', async () => {
    const ip = '172.31.99.99'
    // RATE_LIMITS.authVerify.limit = 10 for IP-scoped checks
    for (let i = 0; i < 10; i++) {
      queryMock.mockResolvedValueOnce({ rows: [] }) // investor lookup returns empty
      queryMock.mockResolvedValueOnce({ rows: [] }) // audit
      const res = await POST(makeRequest({ email: `ip-test-${i}@example.com` }, ip))
      expect(res.status).toBe(200)
    }

    const res = await POST(makeRequest({ email: 'final@example.com' }, ip))
    expect(res.status).toBe(429)
  })
})
