import { describe, expect, it } from "vitest";
import { unsafeOpaqueId, type PrincipalId, type SessionId } from "@awp/contracts";
import type { ApplicationTransaction, SessionRecord, UnitOfWork } from "@awp/application";
import { OperatorSessionManager } from "../../../apps/control-plane/src/operator-session.js";

function sessionHarness(start = "2026-08-23T00:00:00.000Z") {
  let now = new Date(start);
  const rows: SessionRecord[] = [];
  let lastSeenUpdates = 0;
  const repository = {
    async getById(id: SessionId) {
      return rows.find((row) => row.id === id);
    },
    async getByTokenHash(tokenHash: string) {
      return rows.find((row) => row.tokenHash === tokenHash);
    },
    async insert(session: SessionRecord) {
      rows.push(session);
    },
    async updateLastSeen(id: SessionId, lastSeenAt: string) {
      const index = rows.findIndex((row) => row.id === id);
      if (index < 0) throw new Error("missing session");
      rows[index] = { ...rows[index]!, lastSeenAt };
      lastSeenUpdates += 1;
    },
    async revoke(id: SessionId, revokedAt: string) {
      const index = rows.findIndex((row) => row.id === id);
      if (index < 0) throw new Error("missing session");
      rows[index] = { ...rows[index]!, revokedAt };
    },
    async revokeAllByPrincipal(principalId: PrincipalId, revokedAt: string) {
      let count = 0;
      for (let index = 0; index < rows.length; index += 1) {
        const row = rows[index]!;
        if (row.principalId !== principalId) continue;
        rows[index] = { ...row, revokedAt };
        count += 1;
      }
      return count;
    },
  };
  const tx = { sessions: repository } as unknown as ApplicationTransaction;
  const uow: UnitOfWork = { transaction: async (work) => work(tx) };
  const principalId = unsafeOpaqueId<PrincipalId>("principal:test-owner");
  const makeManager = (passwordHash: string) =>
    new OperatorSessionManager(uow, {
      passwordHash,
      principalId,
      now: () => new Date(now),
      verifyPassword: async (_hash, password) => password === "correct horse battery staple",
    });
  const manager = makeManager("$argon2id$v=19$m=65536,t=3,p=1$test$test");
  return {
    manager,
    makeManager,
    uow,
    now: () => new Date(now),
    rows,
    lastSeenUpdates: () => lastSeenUpdates,
    advance(ms: number) {
      now = new Date(now.getTime() + ms);
    },
  };
}

describe("ADR-0009 operator sessions", () => {
  it("stores only a SHA-256 token digest and resolves a Human Principal", async () => {
    const setup = sessionHarness();
    expect(await setup.manager.login("wrong")).toBeUndefined();
    expect(setup.rows).toHaveLength(0);

    const created = await setup.manager.login(
      "correct horse battery staple",
      "AWP-Test-Browser/1.0",
    );
    expect(created).toBeDefined();
    expect(created!.token).toMatch(/^[A-Za-z0-9_-]{43}$/u);
    expect(created!.session.tokenHash).toMatch(/^[0-9a-f]{64}$/u);
    expect(created!.session.tokenHash).not.toContain(created!.token);
    expect(JSON.stringify(created!.session)).not.toContain(created!.token);
    expect(created!.session.userAgentDigest).toMatch(/^[0-9a-f]{64}$/u);
    expect(Date.parse(created!.session.expiresAt) - Date.parse(created!.session.createdAt)).toBe(
      30 * 24 * 60 * 60 * 1000,
    );

    await expect(setup.manager.resolve(created!.token)).resolves.toMatchObject({
      id: "principal:test-owner",
      kind: "human",
    });
  });

  it("refreshes last_seen at most once per five minutes and enforces idle expiry", async () => {
    const setup = sessionHarness();
    const created = await setup.manager.login("correct horse battery staple");
    const absoluteExpiry = created!.session.expiresAt;

    setup.advance(4 * 60 * 1000);
    await setup.manager.resolve(created!.token);
    expect(setup.lastSeenUpdates()).toBe(0);

    setup.advance(2 * 60 * 1000);
    await setup.manager.resolve(created!.token);
    expect(setup.lastSeenUpdates()).toBe(1);
    expect(setup.rows[0]!.expiresAt).toBe(absoluteExpiry);

    setup.advance(7 * 24 * 60 * 60 * 1000 + 1);
    await expect(setup.manager.resolve(created!.token)).resolves.toBeUndefined();
    expect(setup.rows[0]!.expiresAt).toBe(absoluteExpiry);
  });

  it("invalidates an existing session when the operator credential hash rotates", async () => {
    const setup = sessionHarness();
    const created = await setup.manager.login("correct horse battery staple");
    expect(created).toBeDefined();
    const rotated = setup.makeManager("$argon2id$v=19$m=65536,t=3,p=1$rotated$hash");

    await expect(rotated.resolve(created!.token)).resolves.toBeUndefined();
    expect(setup.rows[0]?.revokedAt).toBe(setup.now().toISOString());
  });

  it("uses the real Argon2id verifier for an encoded operator hash", async () => {
    const setup = sessionHarness();
    const manager = new OperatorSessionManager(setup.uow, {
      passwordHash:
        "$argon2id$v=19$m=4096,t=1,p=1$Ck40vfEDgiKQLTypU7q/Aw$yXWnK44gWuPM+zdnRSNBq4WbaLVdKlBMm5dT2VY8bOU",
      principalId: unsafeOpaqueId<PrincipalId>("principal:test-owner"),
      now: setup.now,
    });
    await expect(manager.login("wrong")).resolves.toBeUndefined();
    await expect(manager.login("awp-test-password")).resolves.toBeDefined();
  });

  it("revokes one session and can revoke all operator sessions", async () => {
    const setup = sessionHarness();
    const first = await setup.manager.login("correct horse battery staple");
    const second = await setup.manager.login("correct horse battery staple");
    expect(setup.rows).toHaveLength(2);

    await expect(setup.manager.revoke(first!.token)).resolves.toBe(true);
    await expect(setup.manager.resolve(first!.token)).resolves.toBeUndefined();
    await expect(setup.manager.resolve(second!.token)).resolves.toMatchObject({ kind: "human" });

    await expect(setup.manager.revokeAll()).resolves.toBe(2);
    await expect(setup.manager.resolve(second!.token)).resolves.toBeUndefined();
  });
});
