/**
 * Regression test for the "lost access" scenario:
 *
 *   1. Admin invites investor A → token T1 is created and emailed.
 *   2. Investor A opens the link successfully → T1 is marked used_at.
 *   3. Investor A clears their session (or a redeploy drops cookies).
 *   4. Investor A returns to / — redirected to /auth.
 *   5. Without this feature, A is stuck: T1 is already used, expired, or the
 *      session is gone, and there is no self-service way to get back in.
 *   6. With this feature, A enters their email on /auth and the endpoint
 *      issues a brand new, unused magic link T2 for the same investor row.
 *
 * This test wires together the request-link handler with the real verify
 * handler against an in-memory fake of the two tables the flow touches
 * (pitch_investors, pitch_magic_links) so we can assert end-to-end that a
 * second link works after the first one was used.
 */

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

// ---- In-memory fake of the two tables touched by this flow ----

interface InvestorRow {
  id: string
  email: string
  name: string | null
  company: string | null
  status: 'invited' | 'active' | 'revoked'
  last_login_at: Date | null
  login_count: number
}
interface MagicLinkRow {
  id: string
  investor_id: string
  token: string
  expires_at: Date
  used_at: Date | null
  ip_address: string | null
  user_agent: string | null
}

const db = {
  investors: [] as InvestorRow[],
  magicLinks: [] as MagicLinkRow[],
  sessions: [] as { id: string; investor_id: string; ip_address: string | null }[],
}

let idCounter = 0
const nextId = () => `row-${++idCounter}`

// A tiny query router: match the SQL fragment we care about, ignore the rest.
const queryMock = vi.fn(async (sql: string, params: unknown[] = []) => {
  const s = sql.replace(/\s+/g, ' ').trim()

  // Investor lookup by email (used by request-link)
  if (/SELECT id, email, name, status FROM pitch_investors WHERE email = \$1/i.test(s)) {
    const row = db.investors.find(i => i.email === params[0])
    return { rows: row ? [row] : [] }
  }

  // Insert magic link
  if (/INSERT INTO pitch_magic_links \(investor_id, token, expires_at\)/i.test(s)) {
    db.magicLinks.push({
      id: nextId(),
      investor_id: params[0] as string,
      token: params[1] as string,
      expires_at: params[2] as Date,
      used_at: null,
      ip_address: null,
      user_agent: null,
    })
    return { rows: [] }
  }

  // Verify: magic link + investor JOIN lookup
  if (/FROM pitch_magic_links ml JOIN pitch_investors i/i.test(s)) {
    const link = db.magicLinks.find(ml => ml.token === params[0])
    if (!link) return { rows: [] }
    const inv = db.investors.find(i => i.id === link.investor_id)!
    return {
      rows: [{
        id: link.id,
        investor_id: link.investor_id,
        expires_at: link.expires_at,
        used_at: link.used_at,
        email: inv.email,
        investor_status: inv.status,
      }],
    }
  }

  // Mark magic link used
  if (/UPDATE pitch_magic_links SET used_at = NOW/i.test(s)) {
    const link = db.magicLinks.find(ml => ml.id === params[2])
    if (link) {
      link.used_at = new Date()
      link.ip_address = params[0] as string | null
      link.user_agent = params[1] as string | null
    }
    return { rows: [] }
  }

  // Activate investor
  if (/UPDATE pitch_investors SET status = 'active'/i.test(s)) {
    const inv = db.investors.find(i => i.id === params[0])
    if (inv) {
      inv.status = 'active'
      inv.last_login_at = new Date()
      inv.login_count += 1
    }
    return { rows: [] }
  }

  // createSession: revoke prior sessions (no-op in fake)
  if (/UPDATE pitch_sessions SET revoked = true WHERE investor_id/i.test(s)) {
    return { rows: [] }
  }

  // createSession: insert
  if (/INSERT INTO pitch_sessions/i.test(s)) {
    const id = nextId()
    db.sessions.push({ id, investor_id: params[0] as string, ip_address: params[2] as string | null })
    return { rows: [{ id }] }
  }

  // createSession: fetch investor email for JWT
  if (/SELECT email FROM pitch_investors WHERE id = \$1/i.test(s)) {
    const inv = db.investors.find(i => i.id === params[0])
    return { rows: inv ? [{ email: inv.email }] : [] }
  }

  // new-ip detection query (verify route)
  if (/SELECT DISTINCT ip_address FROM pitch_sessions/i.test(s)) {
    return { rows: [] }
  }

  // Audit log insert — accept everything
  if (/INSERT INTO pitch_audit_logs/i.test(s)) {
    return { rows: [] }
  }

  throw new Error(`Unmocked query: ${s.slice(0, 120)}…`)
})

vi.mock('@/lib/db', () => ({
  default: { query: (...args: unknown[]) => queryMock(args[0] as string, args[1] as unknown[]) },
}))

// Capture emails instead of sending them
const sentEmails: Array<{ to: string; url: string }> = []
vi.mock('@/lib/email', () => ({
  sendMagicLinkEmail: vi.fn(async (to: string, _name: string | null, url: string) => {
    sentEmails.push({ to, url })
  }),
}))

// next/headers cookies() needs to be stubbed — setSessionCookie calls it.
vi.mock('next/headers', () => ({
  cookies: async () => ({
    set: vi.fn(),
    get: vi.fn(),
    delete: vi.fn(),
  }),
}))

// Import the handlers AFTER mocks are set up
import { POST as requestLink } from '@/app/api/auth/request-link/route'
import { POST as verifyLink } from '@/app/api/auth/verify/route'

function makeJsonRequest(url: string, body: unknown, ip = '203.0.113.1'): NextRequest {
  return new NextRequest(url, {
    method: 'POST',
    headers: { 'content-type': 'application/json', 'x-forwarded-for': ip },
    body: JSON.stringify(body),
  })
}

function extractToken(url: string): string {
  const m = url.match(/token=([0-9a-f]+)/)
  if (!m) throw new Error(`No token in url: ${url}`)
  return m[1]
}

beforeEach(() => {
  db.investors = []
  db.magicLinks = []
  db.sessions = []
  sentEmails.length = 0
  idCounter = 0
  queryMock.mockClear()
})

describe('Regression: investor can re-request a working magic link after the first is consumed', () => {
  it('full flow — invite → use → request-link → new link works', async () => {
    // --- Setup: admin has already invited the investor (simulate the outcome) ---
    const investorId = 'investor-42'
    db.investors.push({
      id: investorId,
      email: 'vc@example.com',
      name: 'VC Partner',
      company: 'Acme Capital',
      status: 'invited',
      last_login_at: null,
      login_count: 0,
    })
    db.magicLinks.push({
      id: 'ml-original',
      investor_id: investorId,
      token: 'a'.repeat(96), // original invite token
      expires_at: new Date(Date.now() + 72 * 60 * 60 * 1000),
      used_at: null,
      ip_address: null,
      user_agent: null,
    })

    // --- Step 1: investor uses the original invite link ---
    const firstVerify = await verifyLink(makeJsonRequest('http://localhost/api/auth/verify', { token: 'a'.repeat(96) }))
    expect(firstVerify.status).toBe(200)
    const first = db.magicLinks.find(ml => ml.id === 'ml-original')!
    expect(first.used_at).not.toBeNull()

    // --- Step 2: investor comes back later; clicks the same link → rejected ---
    const replay = await verifyLink(makeJsonRequest('http://localhost/api/auth/verify', { token: 'a'.repeat(96) }))
    expect(replay.status).toBe(401)
    const replayBody = await replay.json()
    expect(replayBody.error).toMatch(/already been used/i)

    // --- Step 3: investor visits /auth and submits their email ---
    const reissue = await requestLink(
      makeJsonRequest('http://localhost/api/auth/request-link', { email: 'vc@example.com' }, '203.0.113.99'),
    )
    expect(reissue.status).toBe(200)
    const reissueBody = await reissue.json()
    expect(reissueBody.success).toBe(true)

    // --- Step 4: a fresh email was dispatched to the investor ---
    expect(sentEmails).toHaveLength(1)
    expect(sentEmails[0].to).toBe('vc@example.com')
    const newToken = extractToken(sentEmails[0].url)
    expect(newToken).not.toBe('a'.repeat(96))
    expect(newToken).toMatch(/^[0-9a-f]{96}$/)

    // A second unused magic link row exists for the same investor
    const links = db.magicLinks.filter(ml => ml.investor_id === investorId)
    expect(links).toHaveLength(2)
    const newLink = links.find(ml => ml.token === newToken)!
    expect(newLink.used_at).toBeNull()

    // --- Step 5: the new token validates successfully ---
    const secondVerify = await verifyLink(makeJsonRequest('http://localhost/api/auth/verify', { token: newToken }))
    expect(secondVerify.status).toBe(200)
    const secondBody = await secondVerify.json()
    expect(secondBody.success).toBe(true)
    expect(secondBody.redirect).toBe('/')

    // And the new link is now used, mirroring the one-time-use contract
    expect(newLink.used_at).not.toBeNull()
  })

  it('unknown emails do not create magic links or send email (prevents enumeration & abuse)', async () => {
    // No investors in the DB
    const res = await requestLink(
      makeJsonRequest('http://localhost/api/auth/request-link', { email: 'stranger@example.com' }),
    )
    expect(res.status).toBe(200)
    const body = await res.json()
    // Same generic message as the happy path
    expect(body.success).toBe(true)
    expect(body.message).toMatch(/if this email was invited/i)

    expect(sentEmails).toHaveLength(0)
    expect(db.magicLinks).toHaveLength(0)
  })

  it('revoked investors cannot self-serve a new link', async () => {
    db.investors.push({
      id: 'revoked-1',
      email: 'gone@example.com',
      name: null,
      company: null,
      status: 'revoked',
      last_login_at: null,
      login_count: 0,
    })

    const res = await requestLink(
      makeJsonRequest('http://localhost/api/auth/request-link', { email: 'gone@example.com' }),
    )
    expect(res.status).toBe(200) // generic success (no info leak)
    expect(sentEmails).toHaveLength(0)
    expect(db.magicLinks).toHaveLength(0)
  })
})
