import { beforeEach, describe, expect, it, vi } from 'vitest'

/**
 * "When I change the role and save, it should actually reflect and set the
 * role — and vice versa, driver to broker and so on." (2026-09-11)
 *
 * Exercises the REAL sign-in and account code (accounts.ts, env-users.ts,
 * passwords.ts) against an in-memory stand-in for the `auth_accounts` table,
 * so the whole chain is proven: admin saves a role → the next sign-in gets
 * that role, not the one in AUTH_USERS → old sessions are cut off.
 */

// ---- in-memory stand-in for the auth_accounts table ------------------------
type Row = Record<string, unknown>
const db: { rows: Row[]; tableExists: boolean; down: boolean } = { rows: [], tableExists: true, down: false }
const MISSING = { code: 'PGRST205', message: "Could not find the table 'public.auth_accounts' in the schema cache" }

function fakeFrom() {
  const filters: Array<[string, unknown]> = []
  let upsertRow: Row | null = null
  let single = false
  const builder = {
    select: () => builder,
    limit: () => builder,
    eq: (col: string, val: unknown) => (filters.push([col, val]), builder),
    maybeSingle: () => ((single = true), builder),
    upsert: (row: Row) => ((upsertRow = row), builder),
    then(resolve: (v: { data: unknown; error: unknown }) => void) {
      if (db.down) return resolve({ data: null, error: { code: '08006', message: 'connection failure' } })
      if (!db.tableExists) return resolve({ data: null, error: MISSING })
      if (upsertRow) {
        const i = db.rows.findIndex((r) => r.user_id === upsertRow!.user_id)
        if (i >= 0) db.rows[i] = { ...db.rows[i], ...upsertRow }
        else db.rows.push({ password_hash: null, role: null, ...upsertRow })
        return resolve({ data: null, error: null })
      }
      const hits = db.rows.filter((r) => filters.every(([c, v]) => r[c] === v))
      return resolve({ data: single ? (hits[0] ?? null) : hits, error: null })
    },
  }
  return builder
}
vi.mock('@/lib/supabase/admin', () => ({ createAdminClient: () => ({ from: fakeFrom }) }))

// Test logins only — never the real AUTH_USERS.
process.env.AUTH_USERS = JSON.stringify([
  { username: 'Nash_Turcan', password: 'env-Pass-1', role: 'broker' },
  { username: 'Pat_Driver', password: 'env-Pass-2', role: 'driver' },
])

const { verifyLogin, setAccountRole, setAccountPassword, isSessionRevoked, invalidateAccount } = await import(
  '@/lib/auth/accounts'
)
const { findEnvUserByUsername } = await import('@/lib/auth/env-users')
const nash = findEnvUserByUsername('Nash_Turcan')!
const pat = findEnvUserByUsername('Pat_Driver')!

beforeEach(() => {
  db.rows = []
  db.tableExists = true
  db.down = false
  // The 30-second account cache lives across tests; wiping the table behind
  // its back (which production never does — every write goes through
  // setAccount*, which clears the entry) would otherwise leak one test's
  // state into the next.
  invalidateAccount(nash.id)
  invalidateAccount(pat.id)
})

describe('saving a role takes effect at sign-in', () => {
  it('broker → admin: the next sign-in is an admin, with the admin tooling', async () => {
    expect((await verifyLogin('Nash_Turcan', 'env-Pass-1'))!.role).toBe('broker')
    expect((await setAccountRole(nash, 'admin', 'Test Admin')).ok).toBe(true)
    const signedIn = await verifyLogin('Nash_Turcan', 'env-Pass-1')
    expect(signedIn!.role).toBe('admin')
    expect(signedIn!.internal).toBe(true) // pilot bar + previews are admin-only
  })

  it('and back again — admin → broker removes admin and the tooling', async () => {
    await setAccountRole(nash, 'admin', 'Test Admin')
    await setAccountRole(nash, 'broker', 'Test Admin')
    const signedIn = await verifyLogin('Nash_Turcan', 'env-Pass-1')
    expect(signedIn!.role).toBe('broker')
    expect(signedIn!.internal).toBe(false)
  })

  it('driver → broker → dispatcher: every change is the role at the next sign-in', async () => {
    for (const role of ['broker', 'dispatcher', 'driver', 'admin'] as const) {
      await setAccountRole(pat, role, 'Test Admin')
      expect((await verifyLogin('Pat_Driver', 'env-Pass-2'))!.role).toBe(role)
    }
  })

  it('a saved role never changes anyone else', async () => {
    await setAccountRole(nash, 'admin', 'Test Admin')
    expect((await verifyLogin('Pat_Driver', 'env-Pass-2'))!.role).toBe('driver')
  })

  it('the wrong password still fails after a role change', async () => {
    await setAccountRole(nash, 'admin', 'Test Admin')
    expect(await verifyLogin('Nash_Turcan', 'wrong')).toBeNull()
  })
})

describe('saving ends the old sessions', () => {
  it('a session from before the save is refused; a new sign-in is accepted', async () => {
    await setAccountRole(nash, 'admin', 'Test Admin')
    const savedAt = Math.floor(Date.parse(db.rows[0].tokens_valid_after as string) / 1000)
    expect(await isSessionRevoked(savedAt - 60, [nash.id])).toBe(true)
    expect(await isSessionRevoked(savedAt + 1, [nash.id])).toBe(false)
    // Same second as the save: the fresh sign-in that follows it must work.
    expect(await isSessionRevoked(savedAt, [nash.id])).toBe(false)
  })

  it("an admin's pilot-switched session ends when that admin is changed", async () => {
    await setAccountRole(nash, 'broker', 'Test Admin')
    const savedAt = Math.floor(Date.parse(db.rows[0].tokens_valid_after as string) / 1000)
    // Session belongs to Pat, but was started by Nash through the role bar.
    expect(await isSessionRevoked(savedAt - 60, [pat.id, nash.id])).toBe(true)
  })

  it('people nobody changed keep their sessions', async () => {
    await setAccountRole(nash, 'admin', 'Test Admin')
    expect(await isSessionRevoked(1, [pat.id])).toBe(false)
  })
})

describe('password reset', () => {
  it('replaces the AUTH_USERS password and keeps the saved role', async () => {
    await setAccountRole(nash, 'admin', 'Test Admin')
    await setAccountPassword(nash, 'New-Pass-99', 'Test Admin')
    expect(await verifyLogin('Nash_Turcan', 'env-Pass-1')).toBeNull()
    const signedIn = await verifyLogin('Nash_Turcan', 'New-Pass-99')
    expect(signedIn!.role).toBe('admin')
  })
})

describe('before migration 0015 is applied', () => {
  it('refuses to save and says why, instead of pretending', async () => {
    db.tableExists = false
    const result = await setAccountRole(nash, 'admin', 'Test Admin')
    expect(result.ok).toBe(false)
    expect(result).toMatchObject({ missing: true })
    // …and sign-in keeps working exactly as before, on AUTH_USERS.
    expect((await verifyLogin('Nash_Turcan', 'env-Pass-1'))!.role).toBe('broker')
  })
})

describe('when the database cannot answer at sign-in', () => {
  it('refuses the login rather than letting an old AUTH_USERS password back in', async () => {
    await setAccountPassword(nash, 'New-Pass-99', 'Test Admin') // admin reset it
    invalidateAccount(nash.id)
    db.down = true
    await expect(verifyLogin('Nash_Turcan', 'env-Pass-1')).rejects.toThrow(/temporarily unavailable/)
    await expect(verifyLogin('Nash_Turcan', 'New-Pass-99')).rejects.toThrow(/temporarily unavailable/)
  })
  it('does not sign existing sessions out during the outage', async () => {
    db.down = true
    expect(await isSessionRevoked(1, [nash.id])).toBe(false)
  })
  it('works again as soon as the database is back (the failure is not cached)', async () => {
    await setAccountPassword(nash, 'New-Pass-99', 'Test Admin')
    invalidateAccount(nash.id)
    db.down = true
    await expect(verifyLogin('Nash_Turcan', 'New-Pass-99')).rejects.toThrow()
    db.down = false
    expect((await verifyLogin('Nash_Turcan', 'New-Pass-99'))!.username).toBe('Nash_Turcan')
  })
})

describe('a live session follows the current role — no re-login needed', () => {
  // A 7-day cookie froze role + internal at sign-in. Nash_Turcan was promoted
  // to admin in AUTH_USERS and the old cookie kept hiding every admin page
  // and the pilot bar (feedback, 2026-09-11).
  const stale = { sub: nash.id, email: nash.email, name: nash.name, role: 'broker', company: null, internal: false, iat: 1 }
  let cookie: Record<string, unknown> | null = stale
  vi.doMock('@/lib/auth/session', () => ({ readSession: async () => cookie }))
  vi.doMock('next/navigation', () => ({ redirect: () => {} }))

  it('an old broker cookie gets admin + the pilot tooling once the role is admin', async () => {
    const { getSessionUser } = await import('@/lib/auth')
    cookie = stale
    expect((await getSessionUser())!.role).toBe('broker')
    await setAccountRole(nash, 'admin', 'Test Admin')
    // The save revokes the old cookie; a fresh one still says "broker" if the
    // role came from the env — this is the case the fix is for.
    cookie = { ...stale, iat: Math.floor(Date.now() / 1000) + 5 }
    const me = await getSessionUser()
    expect(me!.role).toBe('admin')
    expect(me!.internal).toBe(true)
  })

  it('a switched-into driver session keeps the pilot bar while the origin is an admin, and loses it on demotion', async () => {
    const { getSessionUser } = await import('@/lib/auth')
    await setAccountRole(nash, 'admin', 'Test Admin')
    cookie = { sub: pat.id, email: pat.email, name: pat.name, role: 'driver', company: null, internal: true, origin_sub: nash.id, iat: Math.floor(Date.now() / 1000) + 5 }
    expect(await getSessionUser()).toMatchObject({ role: 'driver', internal: true, originId: nash.id })
    await setAccountRole(nash, 'broker', 'Test Admin')
    cookie = { ...cookie, iat: Math.floor(Date.now() / 1000) + 5 }
    expect(await getSessionUser()).toMatchObject({ role: 'driver', internal: false })
  })

  it('an account removed from AUTH_USERS has no session', async () => {
    const { getSessionUser } = await import('@/lib/auth')
    cookie = { ...stale, sub: '00000000-0000-4000-8000-000000000000', iat: Math.floor(Date.now() / 1000) }
    expect(await getSessionUser()).toBeNull()
  })
})
