// Set before requiring the auth layer (token.js reads JWT_SECRET at load) and
// db.js (pulled in transitively; the pool builds at load). Closed DB port keeps
// idle connections from holding the process open — these tests are DB-free and
// only exercise the pure token/hash logic of the mobile session service.
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, after } = require('node:test')
const assert = require('node:assert/strict')
const jwt = require('jsonwebtoken')

const sessionService = require('../src/auth/session.service')
const tokenLib = require('../src/auth/token')
const db = require('../src/utils/db')

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

const USER = { id: 42, username: 'mobileuser', role: 'admin' }

test('createMobileSession mints a bearer-validatable access token + opaque refresh token', () => {
  const out = sessionService.createMobileSession(USER, { deviceHash: 'abc', userAgent: 'Android' })

  // Access token validates as a mobile session.
  const session = sessionService.validateBearerToken(out.accessToken)
  assert.ok(session)
  assert.equal(session.userId, USER.id)
  assert.equal(session.authMethod, 'mobile')
  assert.ok(session.sessionId)

  // Refresh token is opaque (not a JWT) and its stored form is the sha256 hash.
  assert.equal(typeof out.refreshToken, 'string')
  assert.ok(out.refreshToken.length >= 40)
  assert.equal(out.refreshHash, sessionService.hashRefreshToken(out.refreshToken))
  assert.equal(sessionService.validateBearerToken(out.refreshToken), null, 'refresh token is not a bearer session')

  // Metadata + a future expiry are carried through for the controller to persist.
  assert.equal(out.deviceHash, 'abc')
  assert.equal(out.userAgent, 'Android')
  assert.ok(out.refreshExpiresAt instanceof Date)
  assert.ok(out.refreshExpiresAt.getTime() > Date.now())
})

test('access token is short-lived (mobile TTL, not the 1d web default)', () => {
  const { accessToken } = sessionService.createMobileSession(USER)
  const decoded = jwt.decode(accessToken)
  const lifetime = decoded.exp - decoded.iat
  // Default MOBILE_ACCESS_TTL is 15m — comfortably under the 1d web session.
  assert.ok(lifetime <= 15 * 60, `access token lifetime ${lifetime}s should be <= 15m`)
})

test('refreshMobileSession issues a distinct new pair (rotation)', () => {
  const a = sessionService.createMobileSession(USER)
  const b = sessionService.refreshMobileSession(USER)
  assert.notEqual(a.refreshToken, b.refreshToken)
  assert.notEqual(a.refreshHash, b.refreshHash)
})

test('hashRefreshToken is stable and deterministic', () => {
  assert.equal(sessionService.hashRefreshToken('token-xyz'), sessionService.hashRefreshToken('token-xyz'))
  assert.notEqual(sessionService.hashRefreshToken('a'), sessionService.hashRefreshToken('b'))
  // sha256 hex is 64 chars.
  assert.equal(sessionService.hashRefreshToken('anything').length, 64)
})

test('validateBearerToken rejects a TOTP challenge and garbage', () => {
  const challenge = sessionService.createPartialSession(USER)
  assert.equal(sessionService.validateBearerToken(challenge), null)
  assert.equal(sessionService.validateBearerToken('not-a-jwt'), null)
  assert.equal(sessionService.validateBearerToken(null), null)
})

test('token.signToken honors an expiresIn override, else uses the default', () => {
  const short = tokenLib.signToken(USER, {}, { expiresIn: '1s' })
  const shortDecoded = jwt.decode(short)
  assert.equal(shortDecoded.exp - shortDecoded.iat, 1)

  // No option → historical default (JWT_EXPIRES_IN, 1d) unchanged.
  const dflt = jwt.decode(tokenLib.signToken(USER))
  assert.equal(dflt.exp - dflt.iat, 24 * 60 * 60)
})
