// Pure-logic tests for the mobile SSO bridge MODEL. The .db layer is stubbed
// (plain object exports → mutable in-process), so these are DB-free and only
// exercise the model's gating, hashing, and code-generation logic.
process.env.JWT_SECRET = process.env.JWT_SECRET || 'test-secret'
process.env.DB_HOST = '127.0.0.1'
process.env.DB_PORT = '59999'

const { test, beforeEach, after } = require('node:test')
const assert = require('node:assert/strict')
const crypto = require('crypto')

const bridge = require('../src/model/mobileAuthBridge/mobileAuthBridge.model')
const bridgeDb = require('../src/model/mobileAuthBridge/mobileAuthBridge.db')
const db = require('../src/utils/db')

after(() => db.close())

const sha256 = (s) => crypto.createHash('sha256').update(String(s)).digest('hex')

let calls
beforeEach(() => {
  calls = { insertSession: [], insertCode: [], completeSession: [], markCodeUsed: [], findValidCode: [] }
  bridgeDb.insertSession = async (a) => { calls.insertSession.push(a); return 1 }
  bridgeDb.insertCode = async (a) => { calls.insertCode.push(a); return 1 }
  bridgeDb.completeSession = async (sid, uid) => { calls.completeSession.push([sid, uid]); return 1 }
  bridgeDb.consumeSession = async () => 1
  bridgeDb.getSession = async () => null
  bridgeDb.findValidCode = async (h) => { calls.findValidCode.push(h); return { code_hash: h, user_id: 9, session_id: 's' } }
  bridgeDb.markCodeUsed = async () => 1
  bridgeDb.pruneExpired = async () => 0
})

test('startSession generates a uuid session_id and persists the row', async () => {
  const { sessionId } = await bridge.startSession({
    provider: 'google', codeChallenge: 'chal', redirectUri: 'runicgateway://auth/callback', state: 'st',
  })
  assert.match(sessionId, /^[0-9a-f-]{36}$/)
  assert.equal(calls.insertSession.length, 1)
  assert.equal(calls.insertSession[0].sessionId, sessionId)
  assert.equal(calls.insertSession[0].provider, 'google')
  assert.ok(calls.insertSession[0].expiresAt instanceof Date)
  assert.ok(calls.insertSession[0].expiresAt.getTime() > Date.now())
})

test('issueAuthCode: completes the session, stores only the code HASH, returns the raw code once', async () => {
  const out = await bridge.issueAuthCode({ sessionId: 's-1', userId: 42 })
  assert.ok(out && typeof out.code === 'string')
  // >=128-bit, url-safe, opaque.
  assert.ok(out.code.length >= 22, 'at least 128 bits of base64url entropy')
  assert.match(out.code, /^[A-Za-z0-9_-]+$/)
  // Session was marked completed for THIS user.
  assert.deepEqual(calls.completeSession[0], ['s-1', 42])
  // Only the sha256 hash of the code is persisted; the raw code never is.
  assert.equal(calls.insertCode[0].codeHash, sha256(out.code))
  assert.equal(calls.insertCode[0].userId, 42)
  assert.equal(calls.insertCode[0].sessionId, 's-1')
})

test('issueAuthCode returns null (mints no code) when the session is not pending/eligible', async () => {
  bridgeDb.completeSession = async () => 0 // not pending / expired / already used
  const out = await bridge.issueAuthCode({ sessionId: 's-1', userId: 42 })
  assert.equal(out, null)
  assert.equal(calls.insertCode.length, 0, 'no code row when the session could not be completed')
})

test('findRedeemableCode looks up by the code HASH, not the raw code', async () => {
  const row = await bridge.findRedeemableCode('raw-code-value')
  assert.equal(calls.findValidCode[0], sha256('raw-code-value'))
  assert.equal(row.user_id, 9)
})

test('consumeCode is single-use: true only when a row was actually flipped', async () => {
  bridgeDb.markCodeUsed = async () => 1
  assert.equal(await bridge.consumeCode('c'), true)
  bridgeDb.markCodeUsed = async () => 0 // already used / raced
  assert.equal(await bridge.consumeCode('c'), false)
})

test('two issued codes are distinct (fresh entropy each time)', async () => {
  const a = await bridge.issueAuthCode({ sessionId: 's', userId: 1 })
  const b = await bridge.issueAuthCode({ sessionId: 's', userId: 1 })
  assert.notEqual(a.code, b.code)
})
