import assert from "node:assert/strict";
import { describe, it } from "node:test";

import type { ElyAnalyticsDataPoint, Env } from "../src/bindings.js";
import { authSessionCacheKvKey, authTokenHash } from "../src/auth.js";
import { withAuthenticatedApiControls } from "../src/api_controls.js";
import { handleRequest } from "../src/index.js";
import { jsonResponse } from "../src/responses.js";
import { publicSigningKeysKvKey } from "../src/signing_keys.js";

const PUBLIC_KEY = "a".repeat(64);
const ACCESS_TOKEN = "A".repeat(48);

describe("api controls", () => {
  it("rate limits public API routes before reading KV", async () => {
    const auditEvents: ElyAnalyticsDataPoint[] = [];
    const kvReads: string[] = [];
    const response = await handleRequest(
      new Request("https://elydora.test/api/plugins/signing-keys"),
      testEnv({ auditEvents, kvReads, rateLimitSuccess: false }),
    );

    assert.equal(response.status, 429);
    assert.equal(response.headers.get("retry-after"), "60");
    assert.deepEqual(await response.json(), { error: "rate_limited" });
    assert.deepEqual(kvReads, []);
    assert.equal(auditEvents.length, 1);
    assert.deepEqual(auditEvents[0]?.blobs?.slice(0, 4), [
      "plugins.signing_keys",
      "GET",
      "/api/plugins/signing-keys",
      "rate_limited",
    ]);
    assert.equal(auditEvents[0]?.doubles?.[0], 429);
  });

  it("records successful public API requests", async () => {
    const auditEvents: ElyAnalyticsDataPoint[] = [];
    const rateLimitKeys: string[] = [];
    const response = await handleRequest(
      new Request("https://elydora.test/api/plugins/signing-keys", {
        headers: { "cf-ray": "ray-1", "user-agent": "ely-test" },
      }),
      testEnv({ auditEvents, rateLimitKeys }),
    );

    assert.equal(response.status, 200);
    assert.deepEqual(rateLimitKeys, ["local:plugins.signing_keys"]);
    assert.equal(auditEvents.length, 1);
    assert.deepEqual(auditEvents[0]?.indexes, ["local"]);
    assert.deepEqual(auditEvents[0]?.blobs, [
      "plugins.signing_keys",
      "GET",
      "/api/plugins/signing-keys",
      "handled",
      "ray-1",
      "ely-test",
    ]);
    assert.equal(auditEvents[0]?.doubles?.[0], 200);
  });

  it("records method rejections without consuming rate limit tokens", async () => {
    const auditEvents: ElyAnalyticsDataPoint[] = [];
    const rateLimitKeys: string[] = [];
    const response = await handleRequest(
      new Request("https://elydora.test/api/plugins/signing-keys", { method: "POST" }),
      testEnv({ auditEvents, rateLimitKeys }),
    );

    assert.equal(response.status, 405);
    assert.equal(response.headers.get("allow"), "GET");
    assert.deepEqual(rateLimitKeys, []);
    assert.deepEqual(auditEvents[0]?.blobs?.slice(0, 4), [
      "plugins.signing_keys",
      "POST",
      "/api/plugins/signing-keys",
      "method_not_allowed",
    ]);
    assert.equal(auditEvents[0]?.doubles?.[0], 405);
  });

  it("rate limits authenticated API routes before reading session cache", async () => {
    const auditEvents: ElyAnalyticsDataPoint[] = [];
    const kvReads: string[] = [];
    const response = await withAuthenticatedApiControls(
      new Request("https://elydora.test/api/devices", {
        headers: { authorization: `Bearer ${ACCESS_TOKEN}` },
      }),
      testEnv({ auditEvents, kvReads, rateLimitSuccess: false }),
      "devices.list",
      ["GET"],
      () => Promise.resolve(jsonResponse({ ok: true }, 200)),
    );

    assert.equal(response.status, 429);
    assert.deepEqual(kvReads, []);
    assert.deepEqual(auditEvents[0]?.blobs?.slice(0, 4), [
      "devices.list",
      "GET",
      "/api/devices",
      "rate_limited",
    ]);
  });

  it("rejects missing authenticated API credentials after rate limit", async () => {
    const auditEvents: ElyAnalyticsDataPoint[] = [];
    const rateLimitKeys: string[] = [];
    const response = await withAuthenticatedApiControls(
      new Request("https://elydora.test/api/devices"),
      testEnv({ auditEvents, rateLimitKeys }),
      "devices.list",
      ["GET"],
      () => Promise.resolve(jsonResponse({ ok: true }, 200)),
    );

    assert.equal(response.status, 401);
    assert.equal(response.headers.get("www-authenticate"), "Bearer");
    assert.deepEqual(await response.json(), { error: "authorization_missing" });
    assert.deepEqual(rateLimitKeys, ["local:devices.list:anonymous"]);
    assert.deepEqual(auditEvents[0]?.blobs?.slice(0, 4), [
      "devices.list",
      "GET",
      "/api/devices",
      "authorization_missing",
    ]);
  });

  it("rejects malformed authenticated API credentials with audit coverage", async () => {
    const auditEvents: ElyAnalyticsDataPoint[] = [];
    const rateLimitKeys: string[] = [];
    const response = await withAuthenticatedApiControls(
      new Request("https://elydora.test/api/devices", {
        headers: { authorization: "Bearer short" },
      }),
      testEnv({ auditEvents, rateLimitKeys }),
      "devices.list",
      ["GET"],
      () => Promise.resolve(jsonResponse({ ok: true }, 200)),
    );

    assert.equal(response.status, 401);
    assert.deepEqual(await response.json(), { error: "authorization_invalid" });
    assert.deepEqual(rateLimitKeys, ["local:devices.list:authorization_invalid"]);
    assert.deepEqual(auditEvents[0]?.blobs?.slice(0, 4), [
      "devices.list",
      "GET",
      "/api/devices",
      "authorization_invalid",
    ]);
  });

  it("passes authenticated session context and records subject audit fields", async () => {
    const tokenHash = await authTokenHash(ACCESS_TOKEN);
    const auditEvents: ElyAnalyticsDataPoint[] = [];
    const kvReads: string[] = [];
    const rateLimitKeys: string[] = [];
    let receivedTokenHash = "";
    const response = await withAuthenticatedApiControls(
      new Request("https://elydora.test/api/devices", {
        headers: {
          authorization: `Bearer ${ACCESS_TOKEN}`,
          "cf-ray": "ray-auth",
          "user-agent": "ely-auth-test",
        },
      }),
      testEnv({
        auditEvents,
        kvReads,
        rateLimitKeys,
        kvEntries: [[authSessionCacheKvKey("local", tokenHash), sessionDocument()]],
      }),
      "devices.list",
      ["GET"],
      (context) => {
        receivedTokenHash = context.tokenHash;
        return Promise.resolve(
          jsonResponse({ user_id: context.userId, device_id: context.deviceId }, 200),
        );
      },
    );

    assert.equal(response.status, 200);
    assert.deepEqual(await response.json(), { user_id: "user-01", device_id: "device-01" });
    assert.deepEqual(rateLimitKeys, [`local:devices.list:bearer:${tokenHash}`]);
    assert.deepEqual(kvReads, [authSessionCacheKvKey("local", tokenHash)]);
    assert.equal(receivedTokenHash, tokenHash);
    assert.deepEqual(auditEvents[0]?.blobs, [
      "devices.list",
      "GET",
      "/api/devices",
      "handled",
      "ray-auth",
      "ely-auth-test",
      "user-01",
      "device-01",
    ]);
  });

  it("uses Better Auth D1 sessions when the session cache is cold", async () => {
    const tokenHash = await authTokenHash(ACCESS_TOKEN);
    const auditEvents: ElyAnalyticsDataPoint[] = [];
    const kvReads: string[] = [];
    const rateLimitKeys: string[] = [];
    const d1Queries: string[] = [];
    const d1Binds: unknown[][] = [];
    const response = await withAuthenticatedApiControls(
      new Request("https://elydora.test/api/devices", {
        headers: { authorization: `Bearer ${ACCESS_TOKEN}` },
      }),
      testEnv({
        auditEvents,
        kvReads,
        rateLimitKeys,
        d1: testD1Database({
          firstRows: [
            {
              id: "session-01",
              userId: "user-01",
              expiresAt: "2099-01-01T00:00:00.000Z",
              deviceId: "device-01",
            },
          ],
          queries: d1Queries,
          binds: d1Binds,
        }),
      }),
      "devices.list",
      ["GET"],
      (context) =>
        Promise.resolve(
          jsonResponse(
            {
              user_id: context.userId,
              session_id: context.sessionId,
              device_id: context.deviceId,
            },
            200,
          ),
        ),
    );

    assert.equal(response.status, 200);
    assert.deepEqual(await response.json(), {
      user_id: "user-01",
      session_id: "session-01",
      device_id: "device-01",
    });
    assert.deepEqual(rateLimitKeys, [`local:devices.list:bearer:${tokenHash}`]);
    assert.deepEqual(kvReads, [authSessionCacheKvKey("local", tokenHash)]);
    assert.equal(d1Queries.length, 1);
    assert.match(d1Queries[0] ?? "", /FROM better_auth_session/);
    assert.deepEqual(d1Binds, [[ACCESS_TOKEN]]);
    assert.deepEqual(auditEvents[0]?.blobs?.slice(0, 7), [
      "devices.list",
      "GET",
      "/api/devices",
      "handled",
      "",
      "",
      "user-01",
    ]);
    assert.equal(auditEvents[0]?.blobs?.[7], "device-01");
  });

  it("rejects expired authenticated sessions", async () => {
    const tokenHash = await authTokenHash(ACCESS_TOKEN);
    const response = await withAuthenticatedApiControls(
      new Request("https://elydora.test/api/devices", {
        headers: { authorization: `Bearer ${ACCESS_TOKEN}` },
      }),
      testEnv({
        kvEntries: [
          [
            authSessionCacheKvKey("local", tokenHash),
            sessionDocument("2026-01-01T00:00:00.000Z"),
          ],
        ],
      }),
      "devices.list",
      ["GET"],
      () => Promise.resolve(jsonResponse({ ok: true }, 200)),
    );

    assert.equal(response.status, 401);
    assert.deepEqual(await response.json(), { error: "session_expired" });
  });
});

interface TestEnvOptions {
  auditEvents?: ElyAnalyticsDataPoint[];
  d1?: Env["ELY_DB"];
  kvEntries?: [string, string][];
  kvReads?: string[];
  rateLimitKeys?: string[];
  rateLimitSuccess?: boolean;
}

function testEnv(options: TestEnvOptions = {}): Env {
  const values = new Map<string, string>([
    [
      publicSigningKeysKvKey("local"),
      JSON.stringify({
        version: 1,
        keys: [{ key_id: "elydora-alpha-plugins", public_key: PUBLIC_KEY }],
      }),
    ],
  ]);
  for (const [key, value] of options.kvEntries ?? []) {
    values.set(key, value);
  }

  return {
    ELY_ENVIRONMENT: "local",
    ELY_AUTH_BASE_URL: "https://elydora.test",
    ELY_AUTH_SECRET: "test-auth-secret-for-api-controls",
    ELY_DB: options.d1 ?? testD1Database(),
    ELY_KV: {
      get(key: string): Promise<string | null> {
        options.kvReads?.push(key);
        return Promise.resolve(values.get(key) ?? null);
      },
      delete(key: string): Promise<void> {
        values.delete(key);
        return Promise.resolve();
      },
    },
    ELY_STORAGE: testR2Bucket(),
    ELY_RATE_LIMITER: {
      limit(input: { key: string }): Promise<{ success: boolean }> {
        options.rateLimitKeys?.push(input.key);
        return Promise.resolve({ success: options.rateLimitSuccess ?? true });
      },
    },
    ELY_API_AUDIT: {
      writeDataPoint(event?: ElyAnalyticsDataPoint): void {
        if (event !== undefined) {
          options.auditEvents?.push(event);
        }
      },
    },
    ELY_DIAGNOSTICS: {
      writeDataPoint(): void {},
    },
  };
}

function sessionDocument(expiresAt = "2099-01-01T00:00:00.000Z"): string {
  return JSON.stringify({
    version: 1,
    user_id: "user-01",
    session_id: "session-01",
    device_id: "device-01",
    expires_at: expiresAt,
  });
}

function testR2Bucket(): Env["ELY_STORAGE"] {
  return {
    get() {
      return Promise.resolve(null);
    },
    put() {
      return Promise.resolve({
        arrayBuffer() {
          return Promise.resolve(new ArrayBuffer(0));
        },
      });
    },
    delete() {
      return Promise.resolve();
    },
  };
}

interface TestD1DatabaseOptions {
  binds?: unknown[][];
  firstRows?: unknown[];
  queries?: string[];
}

function testD1Database(options: TestD1DatabaseOptions = {}): Env["ELY_DB"] {
  let firstIndex = 0;
  return {
    prepare(query: string) {
      options.queries?.push(query);
      return testD1PreparedStatement(options, () => firstIndex++);
    },
    batch() {
      return Promise.resolve([]);
    },
    exec() {
      return Promise.resolve({});
    },
  };
}

function testD1PreparedStatement(
  options: TestD1DatabaseOptions,
  nextFirstIndex: () => number,
): ReturnType<Env["ELY_DB"]["prepare"]> {
  return {
    bind(...values: unknown[]) {
      options.binds?.push(values);
      return this;
    },
    first<T>() {
      return Promise.resolve((options.firstRows?.[nextFirstIndex()] as T | undefined) ?? null);
    },
    all() {
      return Promise.resolve({ results: [] });
    },
    run() {
      return Promise.resolve({});
    },
  };
}
