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

import type { ElyR2Bucket, ElyR2PutOptions } from "../src/bindings.js";
import {
  StorageObjectError,
  crashAttachmentKey,
  deleteKnownObject,
  exportObjectKey,
  getVerifiedObject,
  pluginAssetKey,
  pluginPackageKey,
  putVerifiedObject,
  syncPayloadKey,
  syncSnapshotKey,
  userAvatarKey,
} from "../src/storage.js";

const USER_HASH = "a".repeat(64);
const PAYLOAD_HASH = "b".repeat(64);
const PACKAGE_HASH = "c".repeat(64);
const ASSET_HASH = "d".repeat(64);
const AVATAR_HASH = "e".repeat(64);
const ATTACHMENT_HASH = "f".repeat(64);

describe("R2 storage contracts", () => {
  it("builds PRD object keys without sensitive path segments", () => {
    assert.equal(
      syncPayloadKey({
        region: "us-east",
        userHash: USER_HASH,
        objectType: "tabs",
        objectId: "tab-01",
        payloadHash: PAYLOAD_HASH,
      }),
      `sync-payloads/us-east/${USER_HASH}/tabs/tab-01/${PAYLOAD_HASH}.bin`,
    );
    assert.equal(
      syncSnapshotKey({ region: "us-east", userHash: USER_HASH, snapshotId: "snapshot-01" }),
      `sync-snapshots/us-east/${USER_HASH}/snapshot-01.bin`,
    );
    assert.equal(
      pluginPackageKey({ pluginId: "elydora.reader", packageHash: PACKAGE_HASH }),
      `plugin-packages/elydora.reader/${PACKAGE_HASH}.rplug`,
    );
    assert.equal(
      pluginAssetKey({ pluginId: "elydora.reader", assetHash: ASSET_HASH }),
      `plugin-assets/elydora.reader/${ASSET_HASH}`,
    );
    assert.equal(
      userAvatarKey({ userHash: USER_HASH, avatarHash: AVATAR_HASH }),
      `user-avatars/${USER_HASH}/${AVATAR_HASH}`,
    );
    assert.equal(
      crashAttachmentKey({ reportId: "report-01", attachmentHash: ATTACHMENT_HASH }),
      `crash-attachments/report-01/${ATTACHMENT_HASH}`,
    );
    assert.equal(
      exportObjectKey({ userHash: USER_HASH, exportId: "export-01" }),
      `exports/${USER_HASH}/export-01.bin`,
    );
  });

  it("rejects R2 keys with path traversal or unsupported sync object types", () => {
    assert.throws(
      () =>
        syncPayloadKey({
          region: "us-east",
          userHash: USER_HASH,
          objectType: "passwords",
          objectId: "tab-01",
          payloadHash: PAYLOAD_HASH,
        }),
      StorageObjectError,
    );
    assert.throws(
      () => crashAttachmentKey({ reportId: "../report-01", attachmentHash: ATTACHMENT_HASH }),
      StorageObjectError,
    );
  });

  it("rejects raw sync payload keys with unsupported object types before R2 writes", async () => {
    const bucket = recordedR2Bucket();
    const payload = bytes("encrypted sync payload");
    const checksum = sha256(payload);
    const key = `sync-payloads/us-east/${USER_HASH}/passwords/item-01/${checksum}.bin`;

    await assert.rejects(
      () => putVerifiedObject(bucket, key, payload, checksum, "application/octet-stream"),
      StorageObjectError,
    );
    assert.equal(bucket.puts.length, 0);
  });

  it("writes R2 objects only after checksum verification", async () => {
    const bucket = recordedR2Bucket();
    const payload = bytes("encrypted sync payload");
    const checksum = sha256(payload);
    const key = syncPayloadKey({
      region: "us-east",
      userHash: USER_HASH,
      objectType: "tabs",
      objectId: "tab-01",
      payloadHash: checksum,
    });

    const stored = await putVerifiedObject(bucket, key, payload, checksum, "application/octet-stream");

    assert.deepEqual(stored, { key, sha256: checksum, sizeBytes: payload.byteLength });
    assert.equal(bucket.puts.length, 1);
    assert.equal(bucket.puts[0]?.key, key);
    assert.equal(bucket.puts[0]?.options.httpMetadata?.contentType, "application/octet-stream");
    assert.equal(bucket.puts[0]?.options.customMetadata?.sha256, checksum);
    assert.deepEqual(
      new Uint8Array(bucket.puts[0]?.options.sha256 ?? new ArrayBuffer(0)),
      new Uint8Array(Buffer.from(checksum, "hex")),
    );
  });

  it("rejects payload checksums that disagree with hashed R2 key segments", async () => {
    const bucket = recordedR2Bucket();
    const payload = bytes("encrypted sync payload");
    const checksum = sha256(payload);
    const key = syncPayloadKey({
      region: "us-east",
      userHash: USER_HASH,
      objectType: "tabs",
      objectId: "tab-01",
      payloadHash: PAYLOAD_HASH,
    });

    await assert.rejects(
      () => putVerifiedObject(bucket, key, payload, checksum, "application/octet-stream"),
      StorageObjectError,
    );
    assert.equal(bucket.puts.length, 0);
  });

  it("rejects payload checksum mismatches before R2 writes", async () => {
    const bucket = recordedR2Bucket();
    const payload = bytes("encrypted sync payload");
    const key = syncPayloadKey({
      region: "us-east",
      userHash: USER_HASH,
      objectType: "tabs",
      objectId: "tab-01",
      payloadHash: PAYLOAD_HASH,
    });

    await assert.rejects(
      () => putVerifiedObject(bucket, key, payload, PAYLOAD_HASH, "application/octet-stream"),
      StorageObjectError,
    );
    assert.equal(bucket.puts.length, 0);
  });

  it("verifies R2 downloads against the expected checksum", async () => {
    const payload = bytes("encrypted snapshot");
    const checksum = sha256(payload);
    const bucket = recordedR2Bucket(payload);
    const key = syncSnapshotKey({
      region: "us-east",
      userHash: USER_HASH,
      snapshotId: "snapshot-01",
    });

    const downloaded = await getVerifiedObject(bucket, key, checksum);

    assert.deepEqual(new Uint8Array(downloaded ?? new ArrayBuffer(0)), new Uint8Array(payload));
  });

  it("deletes only known object key shapes", async () => {
    const bucket = recordedR2Bucket();
    const key = syncSnapshotKey({
      region: "us-east",
      userHash: USER_HASH,
      snapshotId: "snapshot-01",
    });

    await deleteKnownObject(bucket, key);

    assert.deepEqual(bucket.deletes, [key]);
    await assert.rejects(
      () => deleteKnownObject(bucket, "sync-snapshots/../bad.bin"),
      StorageObjectError,
    );
    assert.deepEqual(bucket.deletes, [key]);
  });
});

interface RecordedPut {
  key: string;
  payload: ArrayBuffer;
  options: ElyR2PutOptions;
}

interface RecordedR2Bucket extends ElyR2Bucket {
  deletes: string[];
  puts: RecordedPut[];
}

function recordedR2Bucket(payload?: ArrayBuffer): RecordedR2Bucket {
  const deletes: string[] = [];
  const puts: RecordedPut[] = [];
  return {
    deletes,
    puts,
    get() {
      if (payload === undefined) {
        return Promise.resolve(null);
      }
      return Promise.resolve({
        arrayBuffer() {
          return Promise.resolve(payload);
        },
      });
    },
    put(key: string, value: ArrayBuffer, options: ElyR2PutOptions = {}) {
      puts.push({ key, payload: value, options });
      return Promise.resolve({
        arrayBuffer() {
          return Promise.resolve(value);
        },
      });
    },
    delete(key: string) {
      deletes.push(key);
      return Promise.resolve();
    },
  };
}

function bytes(value: string): ArrayBuffer {
  return new TextEncoder().encode(value).buffer;
}

function sha256(payload: ArrayBuffer): string {
  return createHash("sha256").update(new Uint8Array(payload)).digest("hex");
}
