import assert from "node:assert/strict";
import crypto from "node:crypto";
import test from "node:test";
import {
  RADIUS,
  RadiusAttributes,
  buildResponse,
  encodeIntegerAttribute,
  encodeStringAttribute,
  encodeVendorStringAttribute,
  fingerprintDevice,
  integerAttribute,
  normalizeDeviceId,
  parsePacket,
  stringAttribute,
  verifyAccountingRequest,
  verifyMessageAuthenticator,
} from "../src/radius.mjs";

function packet(code, identifier, authenticator, attributes) {
  const body = Buffer.concat(attributes);
  const result = Buffer.alloc(20 + body.length);
  result[0] = code;
  result[1] = identifier;
  result.writeUInt16BE(result.length, 2);
  authenticator.copy(result, 4);
  body.copy(result, 20);
  return result;
}

function accountingRequest(secret, attributes) {
  const request = packet(RADIUS.ACCOUNTING_REQUEST, 22, Buffer.alloc(16), attributes);
  crypto.createHash("md5").update(request).update(secret).digest().copy(request, 4);
  return request;
}

function accessRequestWithMessageAuthenticator(secret, attributes) {
  const messageAuthenticator = Buffer.concat([Buffer.from([RadiusAttributes.MESSAGE_AUTHENTICATOR, 18]), Buffer.alloc(16)]);
  const request = packet(RADIUS.ACCESS_REQUEST, 23, crypto.randomBytes(16), [...attributes, messageAuthenticator]);
  crypto.createHmac("md5", secret).update(request).digest().copy(request, request.length - 16);
  return request;
}

test("normalizes common router MAC formats to the same fingerprint", () => {
  assert.equal(normalizeDeviceId("AA-BB-CC-DD-EE-FF"), "aabbccddeeff");
  assert.equal(normalizeDeviceId("aa:bb:cc:dd:ee:ff"), "aabbccddeeff");
  assert.equal(fingerprintDevice("AA-BB-CC-DD-EE-FF"), fingerprintDevice("aa:bb:cc:dd:ee:ff"));
});

test("parses Access-Request and builds a valid Access-Accept", () => {
  const request = parsePacket(packet(
    RADIUS.ACCESS_REQUEST,
    7,
    Buffer.from("00112233445566778899aabbccddeeff", "hex"),
    [encodeStringAttribute(RadiusAttributes.CALLING_STATION_ID, "AA-BB-CC-DD-EE-FF")],
  ));
  assert.equal(stringAttribute(request, RadiusAttributes.CALLING_STATION_ID), "AA-BB-CC-DD-EE-FF");

  const response = buildResponse(RADIUS.ACCESS_ACCEPT, request, "shared-secret", [
    encodeIntegerAttribute(RadiusAttributes.SESSION_TIMEOUT, 900),
  ]);
  const parsed = parsePacket(response);
  assert.equal(parsed.code, RADIUS.ACCESS_ACCEPT);
  assert.equal(integerAttribute(parsed, RadiusAttributes.SESSION_TIMEOUT), 900);

  const expected = crypto.createHash("md5").update(Buffer.concat([
    response.subarray(0, 4), request.authenticator, response.subarray(20), Buffer.from("shared-secret"),
  ])).digest("hex");
  assert.equal(response.subarray(4, 20).toString("hex"), expected);
});

test("accepts only correctly authenticated Accounting-Requests", () => {
  const request = parsePacket(accountingRequest("accounting-secret", [
    encodeIntegerAttribute(RadiusAttributes.ACCT_STATUS_TYPE, 1),
    encodeStringAttribute(RadiusAttributes.ACCT_SESSION_ID, "session-123"),
  ]));
  assert.equal(verifyAccountingRequest(request, "accounting-secret"), true);
  assert.equal(verifyAccountingRequest(request, "wrong-secret"), false);
});

test("verifies and returns the Message-Authenticator required by current RouterOS", () => {
  const request = parsePacket(accessRequestWithMessageAuthenticator("modern-secret", [
    encodeStringAttribute(RadiusAttributes.CALLING_STATION_ID, "AA-BB-CC-DD-EE-FF"),
  ]));
  assert.equal(verifyMessageAuthenticator(request, "modern-secret"), true);
  assert.equal(verifyMessageAuthenticator(request, "wrong-secret"), false);

  const response = buildResponse(RADIUS.ACCESS_ACCEPT, request, "modern-secret", [
    encodeIntegerAttribute(RadiusAttributes.SESSION_TIMEOUT, 3600),
  ], true);
  const parsed = parsePacket(response);
  const detail = parsed.attributeDetails.find((attribute) => attribute.type === RadiusAttributes.MESSAGE_AUTHENTICATOR);
  assert.ok(detail);
  const supplied = Buffer.from(response.subarray(detail.valueOffset, detail.valueOffset + 16));
  const copy = Buffer.from(response);
  request.authenticator.copy(copy, 4);
  copy.fill(0, detail.valueOffset, detail.valueOffset + 16);
  const expected = crypto.createHmac("md5", "modern-secret").update(copy).digest();
  assert.deepEqual(supplied, expected);
});

test("encodes MikroTik vendor rate limits", () => {
  const attribute = encodeVendorStringAttribute(14988, 8, "2000k/5000k");
  assert.equal(attribute[0], RadiusAttributes.VENDOR_SPECIFIC);
  assert.equal(attribute.readUInt32BE(2), 14988);
  assert.equal(attribute[6], 8);
  assert.equal(attribute.subarray(8).toString("utf8"), "2000k/5000k");
});

test("rejects malformed attributes without reading past the packet", () => {
  const malformed = packet(RADIUS.ACCESS_REQUEST, 1, Buffer.alloc(16), [Buffer.from([31, 8, 65])]);
  assert.throws(() => parsePacket(malformed), /attribute length/i);
});
