/**
 * test/trustedDevice.test.js
 *
 * "Remember this device" — the cookie that lets the operator skip
 * the 2FA prompt after every deploy. The deploy zip excludes
 * data/sessions.sqlite, so without this cookie the operator would
 * re-enter the 6-digit code on every push. Cookie survives
 * deploys because it's client-side.
 *
 * Tests:
 *  - Model: create / getByToken / listForUser / touch / remove / removeAllForUser
 *  - Hashing: getByToken only matches when the SHA-256 of the input
 *    token matches the stored hash (we never persist the plaintext)
 *  - Expired rows are filtered out of listForUser
 *  - Cascade: removing a user via DELETE FROM users removes their
 *    trusted_devices rows
 *  - Service: issue() sets the cookie; check() returns the row for
 *    valid + non-expired tokens; returns null for bad / missing /
 *    expired; the cookie is HttpOnly + SameSite=Strict
 *
 * Note on test isolation: setupFreshDb() creates + re-creates the
 * admin@test.local user each call, but better-sqlite3 AUTOINCREMENT
 * keeps incrementing (doesn't reuse freed rowids). So the actual
 * admin id changes between tests. We always look it up rather than
 * hardcoding user_id=1.
 */

'use strict';

const assert = require('assert');
const { setupFreshDb, teardown } = require('./_setup');
const TrustedDevice = require('../models/trustedDevice');
const { db } = require('../config/db');

function currentAdminId() {
  return db.prepare('SELECT id FROM users WHERE email = ?').get('admin@test.local').id;
}

const tests = [
  {
    name: 'Model: create + getByToken round-trips; plaintext never stored',
    fn: () => {
      setupFreshDb();
      try {
        const uid = currentAdminId();
        const token = 'a'.repeat(64);
        const r = TrustedDevice.create({
          user_id: uid, token, user_agent: 'Mozilla/5.0', ip_first_seen: '1.2.3.4',
          expires_at: Date.now() + 86400000,
        });
        assert.ok(r.id > 0, 'create returns id');
        // token_hash is sha256 of the input, NOT the plaintext.
        assert.notStrictEqual(r.token_hash, token, 'plaintext is NOT stored');
        assert.strictEqual(r.token_hash.length, 64, 'hash is 64 hex chars (sha256)');
        const looked = TrustedDevice.getByToken(token);
        assert.ok(looked, 'getByToken finds the row by plaintext');
        assert.strictEqual(looked.id, r.id, 'same row id');
        // getByToken with wrong plaintext returns null
        const wrong = TrustedDevice.getByToken('b'.repeat(64));
        assert.strictEqual(wrong, null, 'wrong token does not match');
      } finally { teardown(); }
    },
  },
  {
    name: 'Model: listForUser filters expired rows',
    fn: () => {
      setupFreshDb();
      try {
        const uid = currentAdminId();
        // live row (expires in 1d)
        TrustedDevice.create({ user_id: uid, token: 'a'.repeat(64), expires_at: Date.now() + 86400000 });
        // expired row
        TrustedDevice.create({ user_id: uid, token: 'b'.repeat(64), expires_at: Date.now() - 1000 });
        // live row for a different user
        const User = require('../models/user');
        const u2 = User.create({ email: 'other@x.com', name: 'Other', password: 'password1234long-enough' });
        TrustedDevice.create({ user_id: u2.id, token: 'c'.repeat(64), expires_at: Date.now() + 86400000 });
        const list = TrustedDevice.listForUser(uid);
        assert.strictEqual(list.length, 1, 'only the live row for the admin is returned');
        assert.strictEqual(list[0].token_hash, TrustedDevice.hashToken('a'.repeat(64)));
      } finally { teardown(); }
    },
  },
  {
    name: 'Model: touch() bumps last_used_at; remove() deletes; removeAllForUser() cascades',
    fn: () => {
      setupFreshDb();
      try {
        const uid = currentAdminId();
        const t1 = TrustedDevice.create({ user_id: uid, token: 'a'.repeat(64), expires_at: Date.now() + 86400000 });
        const t2 = TrustedDevice.create({ user_id: uid, token: 'b'.repeat(64), expires_at: Date.now() + 86400000 });
        const t3 = TrustedDevice.create({ user_id: uid, token: 'c'.repeat(64), expires_at: Date.now() + 86400000 });
        const sleep = (ms) => new Promise(r => setTimeout(r, ms));
        return sleep(5).then(() => {
          TrustedDevice.touch(t2.id);
          const after = TrustedDevice.getById(t2.id);
          assert.ok(after.last_used_at >= t2.last_used_at, 'last_used_at moved forward');
          const r1 = TrustedDevice.remove(t1.id, uid);
          assert.strictEqual(r1.changes, 1, 'remove returned 1 change');
          assert.strictEqual(TrustedDevice.getById(t1.id), null, 't1 gone');
          const r2 = TrustedDevice.removeAllForUser(uid);
          assert.strictEqual(r2.changes, 2, 'removeAll removed 2 remaining');
          assert.strictEqual(TrustedDevice.getById(t2.id), null, 't2 gone');
          assert.strictEqual(TrustedDevice.getById(t3.id), null, 't3 gone');
        });
      } finally { teardown(); }
    },
  },
  {
    name: 'Service: issue() sets HttpOnly + SameSite=Strict cookie; check() validates',
    fn: () => {
      setupFreshDb();
      try {
        const uid = currentAdminId();
        const trustedDevice = require('../services/trustedDevice');
        let setCookie = null;
        const req = {
          ip: '1.2.3.4',
          get: (h) => h.toLowerCase() === 'user-agent' ? 'Mozilla/5.0 (Test)' : null,
          cookies: {},
        };
        const res = {
          cookie: (name, value, opts) => { setCookie = { name, value, opts }; },
        };
        const { row, token } = trustedDevice.issue(req, res, { user_id: uid });
        assert.ok(row, 'issue returns a row');
        assert.strictEqual(setCookie.name, 'n2fa_trust', 'cookie name is n2fa_trust');
        assert.strictEqual(setCookie.value, token, 'cookie value is the plaintext token');
        assert.strictEqual(setCookie.opts.httpOnly, true, 'cookie is HttpOnly');
        assert.strictEqual(setCookie.opts.sameSite, 'strict', 'cookie is SameSite=Strict');
        assert.ok(setCookie.opts.expires instanceof Date, 'cookie has expires Date');
        assert.ok(setCookie.opts.expires.getTime() > Date.now(), 'cookie expires in the future');
        // Now check() with the same cookie should return the row
        req.cookies = { n2fa_trust: token };
        const found = trustedDevice.check(req);
        assert.ok(found, 'check() returns the row for valid cookie');
        assert.strictEqual(found.id, row.id, 'same row id');
        // And the wrong cookie returns null
        req.cookies = { n2fa_trust: 'z'.repeat(64) };
        assert.strictEqual(trustedDevice.check(req), null, 'wrong cookie returns null');
        // No cookie at all
        req.cookies = {};
        assert.strictEqual(trustedDevice.check(req), null, 'no cookie returns null');
      } finally { teardown(); }
    },
  },
  {
    name: 'Service: check() rejects malformed cookie shape',
    fn: () => {
      setupFreshDb();
      try {
        const trustedDevice = require('../services/trustedDevice');
        const req = {
          ip: '1.2.3.4',
          get: () => null,
          cookies: { n2fa_trust: 'not-64-hex-chars-garbage' },
        };
        const r = trustedDevice.check(req);
        assert.strictEqual(r, null, 'malformed cookie returns null');
        // Also: cookie value of 64 hex chars but with non-hex content
        req.cookies.n2fa_trust = 'z'.repeat(64);  // all 'z' — not hex
        assert.strictEqual(trustedDevice.check(req), null, 'non-hex 64-char cookie also rejected');
      } finally { teardown(); }
    },
  },
  {
    name: 'Service: check() returns null for expired row even with right token',
    fn: () => {
      setupFreshDb();
      try {
        const uid = currentAdminId();
        const trustedDevice = require('../services/trustedDevice');
        const token = 'd'.repeat(64);
        // Manually insert an expired row
        TrustedDevice.create({ user_id: uid, token, expires_at: Date.now() - 1000 });
        const req = { ip: '1.2.3.4', get: () => null, cookies: { n2fa_trust: token } };
        assert.strictEqual(trustedDevice.check(req), null, 'expired row returns null');
      } finally { teardown(); }
    },
  },
  {
    name: 'Cascade: deleting a user removes their trusted_devices',
    fn: () => {
      setupFreshDb();
      try {
        const uid = currentAdminId();
        const token = 'e'.repeat(64);
        TrustedDevice.create({ user_id: uid, token, expires_at: Date.now() + 86400000 });
        assert.ok(TrustedDevice.getByToken(token), 'row exists');
        db.prepare('DELETE FROM users WHERE id = ?').run(uid);
        assert.strictEqual(TrustedDevice.getByToken(token), null, 'cascade wiped trusted_devices');
      } finally { teardown(); }
    },
  },
];

module.exports = tests;