"use strict";

const test = require("node:test");
const assert = require("node:assert/strict");
const {
  HoloLakeSessionManager,
} = require("./hololake-session");

function fixture(overrides = {}) {
  let now = 1_800_000_000_000;
  const mail = [];
  const manager = new HoloLakeSessionManager({
    registeredEmails: ["owner@example.invalid"],
    pepper: "test-only-pepper-with-enough-entropy",
    stateFile: "",
    now: () => now,
    sendEmail: async message => {
      mail.push(message);
      return true;
    },
    ...overrides,
  });
  return {
    manager,
    mail,
    advance(milliseconds) {
      now += milliseconds;
    },
  };
}

test("OTP request is non-enumerating and stores no plaintext code", async () => {
  const known = fixture();
  const requested = await known.manager.requestOtp({
    email: "Owner@Example.Invalid",
    deviceId: "ios-device-001",
  });
  assert.equal(requested.accepted, true);
  assert.match(requested.request_id, /^[0-9a-f-]{36}$/);
  assert.equal(known.mail.length, 1);
  assert.match(known.mail[0].text, /\b\d{6}\b/);
  const otp = known.mail[0].text.match(/\b\d{6}\b/)[0];
  assert.doesNotMatch(JSON.stringify(known.manager.inspectState()), new RegExp(otp));

  const unknown = fixture();
  const decoy = await unknown.manager.requestOtp({
    email: "nobody@example.invalid",
    deviceId: "ios-device-001",
  });
  assert.equal(decoy.accepted, requested.accepted);
  assert.match(decoy.request_id, /^[0-9a-f-]{36}$/);
  assert.equal(unknown.mail.length, 0);
});

test("OTP verification is device-bound, attempt-limited, and returns a one-time session token", async () => {
  const state = fixture();
  const requested = await state.manager.requestOtp({
    email: "owner@example.invalid",
    deviceId: "ios-device-001",
  });
  const otp = state.mail[0].text.match(/\b\d{6}\b/)[0];

  const wrongDevice = state.manager.verifyOtp({
    requestId: requested.request_id,
    code: otp,
    deviceId: "ios-device-002",
  });
  assert.equal(wrongDevice.ok, false);
  assert.equal(wrongDevice.error, "invalid_or_expired_code");

  const verified = state.manager.verifyOtp({
    requestId: requested.request_id,
    code: otp,
    deviceId: "ios-device-001",
  });
  assert.equal(verified.ok, true);
  assert.match(verified.session_token, /^[A-Za-z0-9_-]{40,}$/);
  assert.doesNotMatch(
    JSON.stringify(state.manager.inspectState()),
    new RegExp(verified.session_token),
  );

  const reused = state.manager.verifyOtp({
    requestId: requested.request_id,
    code: otp,
    deviceId: "ios-device-001",
  });
  assert.equal(reused.ok, false);
  assert.equal(reused.error, "invalid_or_expired_code");
});

test("session authentication, expiry, and revocation never return the stored token", async () => {
  const state = fixture({ sessionTtlSeconds: 60 });
  const requested = await state.manager.requestOtp({
    email: "owner@example.invalid",
    deviceId: "ios-device-001",
  });
  const otp = state.mail[0].text.match(/\b\d{6}\b/)[0];
  const verified = state.manager.verifyOtp({
    requestId: requested.request_id,
    code: otp,
    deviceId: "ios-device-001",
  });

  const active = state.manager.authenticate(
    verified.session_token,
    "ios-device-001",
  );
  assert.equal(active.ok, true);
  assert.equal(active.session.device_id, "ios-device-001");
  assert.equal(Object.hasOwn(active.session, "token"), false);

  assert.equal(
    state.manager.authenticate(verified.session_token, "ios-device-002").error,
    "session_device_mismatch",
  );
  assert.equal(
    state.manager.revoke(verified.session_token, "ios-device-002").error,
    "session_device_mismatch",
  );
  assert.equal(
    state.manager.revoke(verified.session_token, "ios-device-001").ok,
    true,
  );
  assert.equal(
    state.manager.authenticate(verified.session_token, "ios-device-001").error,
    "session_invalid",
  );

  const second = await state.manager.requestOtp({
    email: "owner@example.invalid",
    deviceId: "ios-device-001",
  });
  const secondOtp = state.mail[1].text.match(/\b\d{6}\b/)[0];
  const secondSession = state.manager.verifyOtp({
    requestId: second.request_id,
    code: secondOtp,
    deviceId: "ios-device-001",
  });
  state.advance(61_000);
  assert.equal(
    state.manager.authenticate(
      secondSession.session_token,
      "ios-device-001",
    ).error,
    "session_expired",
  );
});

test("OTP expires, locks after five failed attempts, and request rate is bounded", async () => {
  const expired = fixture({ otpTtlSeconds: 30 });
  const requested = await expired.manager.requestOtp({
    email: "owner@example.invalid",
    deviceId: "ios-device-001",
  });
  const otp = expired.mail[0].text.match(/\b\d{6}\b/)[0];
  expired.advance(31_000);
  assert.equal(
    expired.manager.verifyOtp({
      requestId: requested.request_id,
      code: otp,
      deviceId: "ios-device-001",
    }).error,
    "invalid_or_expired_code",
  );

  const locked = fixture({ maxOtpAttempts: 5 });
  const lockRequest = await locked.manager.requestOtp({
    email: "owner@example.invalid",
    deviceId: "ios-device-001",
  });
  const realOtp = locked.mail[0].text.match(/\b\d{6}\b/)[0];
  for (let attempt = 0; attempt < 5; attempt += 1) {
    assert.equal(
      locked.manager.verifyOtp({
        requestId: lockRequest.request_id,
        code: "000000",
        deviceId: "ios-device-001",
      }).ok,
      false,
    );
  }
  assert.equal(
    locked.manager.verifyOtp({
      requestId: lockRequest.request_id,
      code: realOtp,
      deviceId: "ios-device-001",
    }).error,
    "invalid_or_expired_code",
  );

  const limited = fixture({ requestLimit: 2 });
  await limited.manager.requestOtp({
    email: "owner@example.invalid",
    deviceId: "ios-device-001",
    networkKey: "198.51.100.7",
  });
  await limited.manager.requestOtp({
    email: "owner@example.invalid",
    deviceId: "ios-device-001",
    networkKey: "198.51.100.7",
  });
  const denied = await limited.manager.requestOtp({
    email: "owner@example.invalid",
    deviceId: "ios-device-001",
    networkKey: "198.51.100.7",
  });
  assert.equal(denied.accepted, false);
  assert.equal(denied.error, "rate_limited");
  assert.equal(limited.mail.length, 2);
});
