import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
  actorKindForPresence,
  clearLocalProfile,
  createDefaultLocalProfile,
  createWebcamFaceConsent,
  decodeLocalProfile,
  encodeLocalProfile,
  initialWebcamFaceConsent,
  loadLocalProfile,
  resolveHumanoidAppearance,
  saveLocalProfile,
  transitionWebcamFaceConsent,
  type LocalProfileV1,
  type ProfileStorageAdapter,
} from "../profile/index.ts";

class MemoryStorage implements ProfileStorageAdapter {
  readonly values = new Map<string, string>();
  getItem(key: string): string | null { return this.values.get(key) ?? null; }
  setItem(key: string, value: string): void { this.values.set(key, value); }
  removeItem(key: string): void { this.values.delete(key); }
}

describe("local member profile", () => {
  it("makes stable, useful defaults from identity without retaining or leaking the id", () => {
    const identityId = "auth0|private-user-82914";
    const first = createDefaultLocalProfile(identityId, "  Avery  ");
    const second = createDefaultLocalProfile(identityId, "Avery");
    assert.deepEqual(first, second);
    assert.equal(first.displayName, "Avery");
    const encoded = encodeLocalProfile(first);
    assert.equal(encoded.includes(identityId), false);
    assert.deepEqual(Object.keys(first).sort(), ["appearance", "displayName", "version"]);
    assert.doesNotThrow(() => JSON.stringify(first));
    const resolved = resolveHumanoidAppearance(first.appearance);
    assert.match(resolved.skinTone, /^#[0-9a-f]{6}$/i);
    assert.match(resolved.outfitColor, /^#[0-9a-f]{6}$/i);
    assert.equal(resolved.bodyShape, first.appearance.bodyShape);
  });

  it("strictly rejects unknown, malformed and future profile data", () => {
    const profile = createDefaultLocalProfile("member", "Member");
    assert.equal(decodeLocalProfile({ ...profile, identityId: "leak" }).ok, false);
    assert.equal(decodeLocalProfile({ ...profile, faceImageUrl: "camera://stream" }).ok, false);
    assert.equal(decodeLocalProfile({ ...profile, appearance: { ...profile.appearance, outfit: "rainbow" } }).ok, false);
    assert.deepEqual(decodeLocalProfile({ version: 99 }), { ok: false, error: "profile: unsupported version" });
    assert.deepEqual(decodeLocalProfile("{"), { ok: false, error: "profile: data is not valid JSON" });
    assert.throws(() => createDefaultLocalProfile("", "Member"), RangeError);
  });

  it("migrates the one exact V1 shape into an isolated V2 appearance", () => {
    const legacy: LocalProfileV1 = {
      version: 1,
      displayName: "River",
      skinTone: "umber",
      outfit: "navy",
      accent: "amber",
      hair: "auburn",
      bodyShape: "slim",
    };
    const decoded = decodeLocalProfile(JSON.stringify(legacy));
    assert.equal(decoded.ok, true);
    if (!decoded.ok) return;
    assert.equal(decoded.migrated, true);
    assert.deepEqual(decoded.profile, {
      version: 2,
      displayName: "River",
      appearance: {
        skinTone: "umber",
        outfit: "navy",
        accent: "amber",
        hair: "auburn",
        bodyShape: "slim",
      },
    });
    assert.equal(decodeLocalProfile({ ...legacy, extra: true }).ok, false);
  });

  it("maps signed-in and anonymous roles by environment", () => {
    assert.equal(actorKindForPresence(true, "outdoors"), "humanoid");
    assert.equal(actorKindForPresence(true, "office"), "humanoid");
    assert.equal(actorKindForPresence(false, "outdoors"), "crow");
    assert.equal(actorKindForPresence(false, "office"), "dog");
  });
});

describe("profile persistence boundary", () => {
  it("round-trips, reports migration without an implicit rewrite, and clears", () => {
    const storage = new MemoryStorage();
    const profile = createDefaultLocalProfile("signed-in-1", "Morgan");
    assert.deepEqual(saveLocalProfile(storage, "tera.profile", profile), { ok: true });
    assert.deepEqual(loadLocalProfile(storage, "tera.profile"), {
      status: "loaded",
      profile,
      migrated: false,
    });

    const legacy: LocalProfileV1 = {
      version: 1,
      displayName: "Morgan",
      skinTone: "sand",
      outfit: "sage",
      accent: "aqua",
      hair: "black",
      bodyShape: "average",
    };
    storage.values.set("legacy", JSON.stringify(legacy));
    const before = storage.values.get("legacy");
    const loaded = loadLocalProfile(storage, "legacy");
    assert.equal(loaded.status, "loaded");
    if (loaded.status === "loaded") assert.equal(loaded.migrated, true);
    assert.equal(storage.values.get("legacy"), before, "load never silently rewrites data");
    assert.deepEqual(clearLocalProfile(storage, "tera.profile"), { ok: true });
    assert.deepEqual(loadLocalProfile(storage, "tera.profile"), { status: "missing" });
  });

  it("contains adapter and encoder exceptions", () => {
    const unavailable: ProfileStorageAdapter = {
      getItem: () => { throw new Error("blocked"); },
      setItem: () => { throw new Error("quota"); },
      removeItem: () => { throw new Error("blocked"); },
    };
    const profile = createDefaultLocalProfile("signed-in-2", "Taylor");
    assert.equal(loadLocalProfile(unavailable, "tera.profile").status, "unavailable");
    assert.equal(saveLocalProfile(unavailable, "tera.profile", profile).ok, false);
    assert.equal(clearLocalProfile(unavailable, "tera.profile").ok, false);
    assert.equal(loadLocalProfile(unavailable, " ").status, "invalid");
    const invalid = { ...profile, displayName: "" };
    assert.equal(saveLocalProfile(new MemoryStorage(), "x", invalid).ok, false);
  });
});

describe("ephemeral webcam face consent", () => {
  it("starts off with explicit privacy invariants and cannot auto-start", () => {
    const state = initialWebcamFaceConsent();
    assert.deepEqual(state, {
      status: "off",
      consentGranted: false,
      ephemeral: true,
      persistence: "none",
      autoFetch: false,
      revision: 0,
    });
    const rejected = transitionWebcamFaceConsent(state, { type: "start", explicitConsent: true });
    assert.equal(rejected.accepted, false);
    if (!rejected.accepted) assert.equal(rejected.reason, "consent-required");
  });

  it("requires request then explicit start, and clears consent on stop", () => {
    const consent = createWebcamFaceConsent();
    assert.equal(consent.requestStart().accepted, true);
    assert.equal(consent.state().status, "awaiting-consent");
    assert.equal(consent.start(true).accepted, true);
    assert.deepEqual(consent.state(), {
      status: "active",
      consentGranted: true,
      ephemeral: true,
      persistence: "none",
      autoFetch: false,
      revision: 2,
    });
    assert.equal(consent.stop().accepted, true);
    assert.equal(consent.state().status, "stopped");
    assert.equal(consent.state().consentGranted, false);
    assert.equal(consent.start(true).accepted, false, "a stopped session needs a new request");
  });

  it("revokes from any live state and requires a fresh user request before reuse", () => {
    const consent = createWebcamFaceConsent();
    consent.requestStart();
    consent.start(true);
    assert.equal(consent.revoke().accepted, true);
    assert.equal(consent.state().status, "revoked");
    assert.equal(consent.state().consentGranted, false);
    assert.equal(consent.start(true).accepted, false);
    assert.equal(consent.requestStart().accepted, true);
    assert.equal(consent.state().status, "awaiting-consent");
    const leaked = consent.state();
    leaked.status = "active";
    assert.equal(consent.state().status, "awaiting-consent");
  });
});
