// Android App Links (M9 follow-up, docs/android/APP_LINKS.md): the web-root
// assetlinks.json verification file and the mobile-SSO redirect allowlist's
// additive https App Link entry. Model is stubbed so these are DB-free.
process.env.JWT_SECRET = process.env.JWT_SECRET || 'test-secret'
process.env.SECRET_ENC_KEY = process.env.SECRET_ENC_KEY || 'unit-test-enc-key'
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 wellKnown = require('../src/router/wellKnown.controller')
const mobileSso = require('../src/router/v1/auth/mobileSso.controller')
const ssoCtrl = require('../src/router/v1/auth/sso.controller')
const settings = require('../src/model/settings/settings.model')
const bridge = require('../src/model/mobileAuthBridge/mobileAuthBridge.model')
const db = require('../src/utils/db')

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

function mockRes() {
  return {
    statusCode: 200,
    body: null,
    headers: {},
    redirectedTo: null,
    status(c) { this.statusCode = c; return this },
    json(b) { this.body = b; return this },
    set(k, v) { this.headers[String(k).toLowerCase()] = v; return this },
    redirect(u) { this.redirectedTo = u; return this },
  }
}

let savedEnabled
beforeEach(() => {
  savedEnabled = settings.isMobileAppLinksEnabled
  delete process.env.APP_BASE_URL
  delete process.env.MOBILE_APP_CERT_SHA256
  delete process.env.MOBILE_APP_PACKAGE
})

function restore() {
  settings.isMobileAppLinksEnabled = savedEnabled
}

// ── /.well-known/assetlinks.json ─────────────────────────────────────────────

test('assetlinks: 404 when App Links are disabled (even if a fingerprint is set)', async () => {
  settings.isMobileAppLinksEnabled = async () => false
  process.env.MOBILE_APP_CERT_SHA256 = 'AB:CD:EF'
  try {
    const res = mockRes()
    await wellKnown.assetlinks({}, res)
    assert.equal(res.statusCode, 404)
  } finally {
    restore()
  }
})

test('assetlinks: 404 when enabled but no fingerprint configured', async () => {
  settings.isMobileAppLinksEnabled = async () => true
  try {
    const res = mockRes()
    await wellKnown.assetlinks({}, res)
    assert.equal(res.statusCode, 404)
  } finally {
    restore()
  }
})

test('assetlinks: enabled + fingerprint → Digital Asset Links statement', async () => {
  settings.isMobileAppLinksEnabled = async () => true
  process.env.MOBILE_APP_CERT_SHA256 = 'ab:cd:ef, 11:22:33'
  try {
    const res = mockRes()
    await wellKnown.assetlinks({}, res)
    assert.equal(res.statusCode, 200)
    assert.ok(Array.isArray(res.body))
    const stmt = res.body[0]
    assert.deepEqual(stmt.relation, ['delegate_permission/common.handle_all_urls'])
    assert.equal(stmt.target.namespace, 'android_app')
    assert.equal(stmt.target.package_name, 'com.runicgateway.app')
    // normalized to upper-case, comma-split into multiple fingerprints
    assert.deepEqual(stmt.target.sha256_cert_fingerprints, ['AB:CD:EF', '11:22:33'])
    assert.match(res.headers['cache-control'], /max-age=3600/)
  } finally {
    restore()
  }
})

test('assetlinks: asserts the fixed published package name', async () => {
  // PACKAGE is resolved at module load from MOBILE_APP_PACKAGE (default below); a
  // white-label build sets that env at boot. Here we assert the shipped default.
  settings.isMobileAppLinksEnabled = async () => true
  process.env.MOBILE_APP_CERT_SHA256 = 'AB:CD'
  try {
    const res = mockRes()
    await wellKnown.assetlinks({}, res)
    assert.equal(res.statusCode, 200)
    assert.equal(res.body[0].target.package_name, 'com.runicgateway.app')
  } finally {
    restore()
  }
})

// ── mobile SSO redirect allowlist ────────────────────────────────────────────

const CUSTOM = 'runicgateway://auth/callback'

function startReq(redirectUri) {
  return {
    query: { provider: 'google', code_challenge: 'chal', state: 'st', redirect_uri: redirectUri },
    ip: '1.2.3.4',
    protocol: 'https',
    get: () => 'play.shard.com',
  }
}

test('allowlist: https self-origin callback is accepted when App Links are enabled', async () => {
  settings.isMobileAppLinksEnabled = async () => true
  bridge.startSession = async () => ({ sessionId: 'sess-1' })
  const savedRedirect = ssoCtrl.redirectToIdp
  ssoCtrl.redirectToIdp = async (req, res) => { res.redirect('https://idp/authorize'); return true }
  try {
    const res = mockRes()
    await mobileSso.start(startReq('https://play.shard.com/mobile/callback'), res)
    assert.equal(res.redirectedTo, 'https://idp/authorize')
    assert.notEqual(res.statusCode, 400)
  } finally {
    ssoCtrl.redirectToIdp = savedRedirect
    restore()
  }
})

test('allowlist: the same https callback is REJECTED when App Links are disabled', async () => {
  settings.isMobileAppLinksEnabled = async () => false
  try {
    const res = mockRes()
    await mobileSso.start(startReq('https://play.shard.com/mobile/callback'), res)
    assert.equal(res.statusCode, 400)
    assert.equal(res.redirectedTo, null)
  } finally {
    restore()
  }
})

test('allowlist: a foreign https host is rejected even when App Links are enabled', async () => {
  settings.isMobileAppLinksEnabled = async () => true
  try {
    const res = mockRes()
    await mobileSso.start(startReq('https://evil.example.com/mobile/callback'), res)
    assert.equal(res.statusCode, 400)
  } finally {
    restore()
  }
})

test('allowlist: wrong path on the self-origin is rejected (exact match, not prefix)', async () => {
  settings.isMobileAppLinksEnabled = async () => true
  try {
    const res = mockRes()
    await mobileSso.start(startReq('https://play.shard.com/mobile/callback/../evil'), res)
    assert.equal(res.statusCode, 400)
  } finally {
    restore()
  }
})

test('allowlist: the custom scheme is always accepted, App Links off', async () => {
  settings.isMobileAppLinksEnabled = async () => false
  bridge.startSession = async () => ({ sessionId: 'sess-1' })
  const savedRedirect = ssoCtrl.redirectToIdp
  ssoCtrl.redirectToIdp = async (req, res) => { res.redirect('https://idp/authorize'); return true }
  try {
    const res = mockRes()
    await mobileSso.start(startReq(CUSTOM), res)
    assert.equal(res.redirectedTo, 'https://idp/authorize')
  } finally {
    ssoCtrl.redirectToIdp = savedRedirect
    restore()
  }
})

test('allowlist: APP_BASE_URL is preferred over the request Host header', async () => {
  settings.isMobileAppLinksEnabled = async () => true
  process.env.APP_BASE_URL = 'https://canonical.shard.com/'
  bridge.startSession = async () => ({ sessionId: 'sess-1' })
  const savedRedirect = ssoCtrl.redirectToIdp
  ssoCtrl.redirectToIdp = async (req, res) => { res.redirect('https://idp/authorize'); return true }
  try {
    // Host header says play.shard.com, but APP_BASE_URL pins the origin.
    const res = mockRes()
    await mobileSso.start(startReq('https://canonical.shard.com/mobile/callback'), res)
    assert.equal(res.redirectedTo, 'https://idp/authorize')
    // A callback matching the (spoofable) Host header but NOT APP_BASE_URL is rejected.
    const res2 = mockRes()
    await mobileSso.start(startReq('https://play.shard.com/mobile/callback'), res2)
    assert.equal(res2.statusCode, 400)
  } finally {
    ssoCtrl.redirectToIdp = savedRedirect
    restore()
  }
})
