import { describe, it, expect, vi } from "vitest";
import { createDsrRouter, dsrRouter } from "@/dsr.router";
import type { DsrTrpcUser } from "@/dsr.router";
import type { DsrBinding } from "@/di/bind-production";
import {
  RecordingDataExport,
  RecordingDataDelete,
  RecordingDataRectify,
  RecordingProcessingRestriction,
} from "@repo/core-testing/instrumentation";

// Recording doubles use local type aliases to avoid circular deps with core-dsr.
// The alias types are structurally compatible at runtime; cast via unknown to
// satisfy the DsrBinding constraint without modifying core-testing.
function makeBinding() {
  return {
    dataExport: new RecordingDataExport(),
    dataDelete: new RecordingDataDelete(),
    dataRectify: new RecordingDataRectify(),
    processingRestriction: new RecordingProcessingRestriction(),
  };
}

type TestBinding = ReturnType<typeof makeBinding>;

function makeCaller(binding: TestBinding, user?: DsrTrpcUser) {
  const router = createDsrRouter(binding as unknown as DsrBinding);
  return router.createCaller({ user } as Record<string, unknown>);
}

const authenticatedUser: DsrTrpcUser = { id: "alice", roles: ["user"] };
const adminUser: DsrTrpcUser = { id: "admin-user", roles: ["admin"] };

describe("dsrRouter.export", () => {
  it("returns UserDataBundle body for authenticated user", async () => {
    const binding = makeBinding();
    const caller = makeCaller(binding, authenticatedUser);
    const result = await caller.export({ subjectId: "alice", format: "json" });

    expect(result.subjectId).toBe("alice");
    expect(result.format).toBe("json");
    expect(binding.dataExport.calls).toHaveLength(1);
  });

  it("works with json-ld format", async () => {
    const binding = makeBinding();
    const caller = makeCaller(binding, authenticatedUser);
    const result = await caller.export({
      subjectId: "alice",
      format: "json-ld",
    });
    expect(result.format).toBe("json-ld");
  });

  it("throws UNAUTHORIZED when ctx.user is absent", async () => {
    const binding = makeBinding();
    const caller = makeCaller(binding, undefined);
    await expect(
      caller.export({ subjectId: "alice", format: "json" }),
    ).rejects.toMatchObject({ code: "UNAUTHORIZED" });
  });

  it("propagates errors from dataExport", async () => {
    const binding = makeBinding();
    vi.spyOn(binding.dataExport, "exportSubjectData").mockRejectedValue(
      new Error("export failed"),
    );
    const caller = makeCaller(binding, authenticatedUser);
    await expect(
      caller.export({ subjectId: "alice", format: "json" }),
    ).rejects.toThrow("export failed");
  });
});

describe("dsrRouter.delete", () => {
  it("returns DeletionCertificate for soft mode (any authenticated user)", async () => {
    const binding = makeBinding();
    const caller = makeCaller(binding, authenticatedUser);
    const result = await caller.delete({ subjectId: "alice", mode: "soft" });

    expect(result.subjectId).toBe("alice");
    expect(result.mode).toBe("soft");
    expect(binding.dataDelete.calls).toHaveLength(1);
  });

  it("returns DeletionCertificate for cascade-hard mode with admin user", async () => {
    const binding = makeBinding();
    const caller = makeCaller(binding, adminUser);
    const result = await caller.delete({
      subjectId: "alice",
      mode: "cascade-hard",
    });

    expect(result.mode).toBe("cascade-hard");
    expect(binding.dataDelete.calls[0]?.mode).toBe("cascade-hard");
  });

  it("throws FORBIDDEN for cascade-hard mode with non-admin user", async () => {
    const binding = makeBinding();
    const caller = makeCaller(binding, authenticatedUser);
    await expect(
      caller.delete({ subjectId: "alice", mode: "cascade-hard" }),
    ).rejects.toMatchObject({ code: "FORBIDDEN" });
    expect(binding.dataDelete.calls).toHaveLength(0);
  });

  it("throws FORBIDDEN for cascade-hard when user has no roles", async () => {
    const binding = makeBinding();
    const noRolesUser: DsrTrpcUser = { id: "alice" };
    const caller = makeCaller(binding, noRolesUser);
    await expect(
      caller.delete({ subjectId: "alice", mode: "cascade-hard" }),
    ).rejects.toMatchObject({ code: "FORBIDDEN" });
  });

  it("throws UNAUTHORIZED when ctx.user is absent", async () => {
    const binding = makeBinding();
    const caller = makeCaller(binding, undefined);
    await expect(
      caller.delete({ subjectId: "alice", mode: "soft" }),
    ).rejects.toMatchObject({ code: "UNAUTHORIZED" });
  });
});

describe("dsrRouter.rectify", () => {
  it("returns { ok: true } for authenticated user", async () => {
    const binding = makeBinding();
    const caller = makeCaller(binding, authenticatedUser);
    const result = await caller.rectify({
      subjectId: "alice",
      collection: "users",
      field: "name",
      value: "Alice New",
    });

    expect(result).toEqual({ ok: true });
    expect(binding.dataRectify.calls[0]).toEqual({
      subjectId: "alice",
      collection: "users",
      field: "name",
      value: "Alice New",
    });
  });

  it("throws UNAUTHORIZED when ctx.user is absent", async () => {
    const binding = makeBinding();
    const caller = makeCaller(binding, undefined);
    await expect(
      caller.rectify({
        subjectId: "alice",
        collection: "users",
        field: "name",
        value: "x",
      }),
    ).rejects.toMatchObject({ code: "UNAUTHORIZED" });
  });

  it("propagates errors from dataRectify", async () => {
    const binding = makeBinding();
    vi.spyOn(binding.dataRectify, "updateSubjectField").mockRejectedValue(
      new Error('Field "secret" is not tagged as PII'),
    );
    const caller = makeCaller(binding, authenticatedUser);
    await expect(
      caller.rectify({
        subjectId: "alice",
        collection: "users",
        field: "secret",
        value: "x",
      }),
    ).rejects.toThrow();
  });
});

describe("dsrRouter.restrict", () => {
  it("returns { ok: true } when granting restriction", async () => {
    const binding = makeBinding();
    const caller = makeCaller(binding, authenticatedUser);
    const result = await caller.restrict({
      subjectId: "alice",
      granted: true,
    });

    expect(result).toEqual({ ok: true });
    expect(binding.processingRestriction.sets[0]).toEqual({
      subjectId: "alice",
      granted: true,
    });
  });

  it("returns { ok: true } when lifting restriction", async () => {
    const binding = makeBinding();
    const caller = makeCaller(binding, authenticatedUser);
    const result = await caller.restrict({
      subjectId: "alice",
      granted: false,
    });
    expect(result).toEqual({ ok: true });
    expect(binding.processingRestriction.sets[0]?.granted).toBe(false);
  });

  it("throws UNAUTHORIZED when ctx.user is absent", async () => {
    const binding = makeBinding();
    const caller = makeCaller(binding, undefined);
    await expect(
      caller.restrict({ subjectId: "alice", granted: true }),
    ).rejects.toMatchObject({ code: "UNAUTHORIZED" });
  });
});

describe("dsrRouter singleton guard", () => {
  it("throws when procedures are called without a real DsrBinding", async () => {
    // The singleton uses a Proxy that throws on any binding property access.
    // Procedures access binding lazily, so the Proxy error surfaces at call time.
    const caller = dsrRouter.createCaller({
      user: authenticatedUser,
    } as Record<string, unknown>);
    await expect(
      caller.export({ subjectId: "alice", format: "json" }),
    ).rejects.toThrow(/dsrRouter singleton/);
  });
});
