/**
 * @file security.test.js
 * @description Tests the network-exposure hardening (GHSA-gr74-4xfh-6jw9):
 * loopback-by-default bind, Host-header allowlist (anti DNS-rebinding),
 * loopback-only CORS, and the optional bearer-token gate on /api/* + WebSocket.
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */
const { describe, it, afterEach } = require("node:test");
const assert = require("node:assert/strict");
const sec = require("../lib/security");

const ENV_KEYS = ["DASHBOARD_HOST", "DASHBOARD_ALLOWED_HOSTS", "DASHBOARD_TOKEN"];
afterEach(() => {
  for (const k of ENV_KEYS) delete process.env[k];
});

function mockRes() {
  return {
    statusCode: 0,
    body: null,
    status(c) {
      this.statusCode = c;
      return this;
    },
    json(b) {
      this.body = b;
      return this;
    },
  };
}

describe("resolveHost", () => {
  it("defaults to loopback (127.0.0.1)", () => {
    assert.equal(sec.resolveHost(), "127.0.0.1");
  });
  it("honors an explicit DASHBOARD_HOST opt-in", () => {
    process.env.DASHBOARD_HOST = "0.0.0.0";
    assert.equal(sec.resolveHost(), "0.0.0.0");
    assert.equal(sec.isLoopbackHostname("0.0.0.0"), true); // treated as loopback-equiv for Host checks
  });
});

describe("Host allowlist (DNS-rebinding defense)", () => {
  it("allows loopback Host headers", () => {
    assert.equal(sec.isHostAllowed("localhost:4820"), true);
    assert.equal(sec.isHostAllowed("127.0.0.1:4820"), true);
    assert.equal(sec.isHostAllowed("[::1]:4820"), true);
    assert.equal(sec.isHostAllowed(""), true); // missing Host (HTTP/1.0 / local tooling)
  });
  it("rejects a rebound attacker Host", () => {
    assert.equal(sec.isHostAllowed("evil.example"), false);
    assert.equal(sec.isHostAllowed("attacker.example:4820"), false);
  });
  it("permits operator-allowlisted hostnames", () => {
    process.env.DASHBOARD_ALLOWED_HOSTS = "dash.internal, 192.168.1.50";
    assert.equal(sec.isHostAllowed("dash.internal:4820"), true);
    assert.equal(sec.isHostAllowed("192.168.1.50:4820"), true);
    assert.equal(sec.isHostAllowed("evil.example"), false);
  });
  it("hostGuard middleware 403s a disallowed Host", () => {
    const res = mockRes();
    let nexted = false;
    sec.hostGuard({ headers: { host: "evil.example" } }, res, () => (nexted = true));
    assert.equal(nexted, false);
    assert.equal(res.statusCode, 403);
    assert.equal(res.body.error.code, "EBADHOST");
  });
  it("hostGuard middleware allows loopback", () => {
    let nexted = false;
    sec.hostGuard({ headers: { host: "localhost:4820" } }, mockRes(), () => (nexted = true));
    assert.equal(nexted, true);
  });
});

describe("CORS", () => {
  const allowed = (origin) =>
    new Promise((resolve) => sec.corsOptions().origin(origin, (_e, ok) => resolve(ok)));

  it("allows same-origin / no-Origin (curl, the server's own client)", async () => {
    assert.equal(await allowed(undefined), true);
  });
  it("allows loopback origins", async () => {
    assert.equal(await allowed("http://localhost:5173"), true);
    assert.equal(await allowed("http://127.0.0.1:4820"), true);
  });
  it("refuses cross-origin pages", async () => {
    assert.equal(await allowed("https://evil.example"), false);
  });
});

describe("token gate (optional, opt-in)", () => {
  it("is a no-op when DASHBOARD_TOKEN is unset (default)", () => {
    let nexted = false;
    sec.tokenGuard({ path: "/stats", headers: {}, query: {} }, mockRes(), () => (nexted = true));
    assert.equal(nexted, true);
  });

  it("rejects a missing/invalid token when configured", () => {
    process.env.DASHBOARD_TOKEN = "s3cret";
    const res = mockRes();
    let nexted = false;
    sec.tokenGuard({ path: "/stats", headers: {}, query: {} }, res, () => (nexted = true));
    assert.equal(nexted, false);
    assert.equal(res.statusCode, 401);
    assert.equal(res.body.error.code, "EUNAUTHORIZED");

    const res2 = mockRes();
    sec.tokenGuard(
      { path: "/stats", headers: { "x-dashboard-token": "wrong" }, query: {} },
      res2,
      () => {}
    );
    assert.equal(res2.statusCode, 401);
  });

  it("accepts a correct token via header, bearer, or query", () => {
    process.env.DASHBOARD_TOKEN = "s3cret";
    const ok = (req) => {
      let nexted = false;
      sec.tokenGuard(req, mockRes(), () => (nexted = true));
      return nexted;
    };
    assert.equal(
      ok({ path: "/stats", headers: { "x-dashboard-token": "s3cret" }, query: {} }),
      true
    );
    assert.equal(
      ok({ path: "/stats", headers: { authorization: "Bearer s3cret" }, query: {} }),
      true
    );
    assert.equal(ok({ path: "/stats", headers: {}, query: { token: "s3cret" } }), true);
  });

  it("exempts health, docs, and local hook ingestion even when a token is set", () => {
    process.env.DASHBOARD_TOKEN = "s3cret";
    const ok = (path) => {
      let nexted = false;
      sec.tokenGuard({ path, headers: {}, query: {} }, mockRes(), () => (nexted = true));
      return nexted;
    };
    assert.equal(ok("/health"), true);
    assert.equal(ok("/openapi.json"), true);
    assert.equal(ok("/hooks/event"), true);
    assert.equal(ok("/sessions/abc"), false); // still gated
  });
});

describe("WebSocket auth", () => {
  it("allows any upgrade when no token is configured", () => {
    assert.equal(sec.isWebSocketAuthorized({ url: "/ws", headers: {} }), true);
  });
  it("requires a matching ?token= when configured", () => {
    process.env.DASHBOARD_TOKEN = "s3cret";
    assert.equal(sec.isWebSocketAuthorized({ url: "/ws?token=s3cret", headers: {} }), true);
    assert.equal(sec.isWebSocketAuthorized({ url: "/ws?token=nope", headers: {} }), false);
    assert.equal(sec.isWebSocketAuthorized({ url: "/ws", headers: {} }), false);
    assert.equal(
      sec.isWebSocketAuthorized({ url: "/ws", headers: { "x-dashboard-token": "s3cret" } }),
      true
    );
  });
});
