/**
 * Tests for admin-moderation DB-layer (server/db/moderation.ts).
 *
 * Covers:
 *   - listModerationQueue: merges posts + comments, sorts by reportedAt desc
 *   - dismissModerationItem: clears flag, writes audit-log
 *   - deleteModerationItem: soft-deletes content, persists snapshot
 *   - banUserFromModerationItem: sets Profile.banned, writes audit-log
 *
 * Strategy: prisma-mock via vi.hoisted (analog demographics.patch.test.ts).
 */
import { describe, expect, it, vi, beforeEach } from "vitest";

const prismaMock = vi.hoisted(() => ({
  communityPost: {
    findMany: vi.fn(),
    findUnique: vi.fn(),
    update: vi.fn(),
  },
  communityReply: {
    findMany: vi.fn(),
    findUnique: vi.fn(),
    update: vi.fn(),
  },
  profile: {
    update: vi.fn(),
  },
  moderationAction: {
    create: vi.fn(),
  },
}));

vi.mock("../../server/utils/prisma", () => ({
  usePrisma: () => prismaMock,
}));

import {
  listModerationQueue,
  dismissModerationItem,
  deleteModerationItem,
  banUserFromModerationItem,
} from "../../server/db/moderation";

beforeEach(() => {
  vi.clearAllMocks();
});

// ─── listModerationQueue ─────────────────────────────────────────────────────

describe("listModerationQueue — merges posts + comments by reportedAt desc", () => {
  it("returns merged sorted list with newest report first", async () => {
    prismaMock.communityPost.findMany.mockResolvedValueOnce([
      {
        id: "post-1",
        userId: "user-1",
        content: "bad post 1",
        reportedAt: new Date("2026-05-01T10:00:00Z"),
        createdAt: new Date("2026-04-30T10:00:00Z"),
        isDeleted: false,
        author: {
          id: "user-1",
          nickname: "alice",
          avatar: null,
          plan: "free",
        },
      },
    ]);
    prismaMock.communityReply.findMany.mockResolvedValueOnce([
      {
        id: "reply-1",
        postId: "post-99",
        userId: "user-2",
        content: "bad comment 1",
        reportedAt: new Date("2026-05-02T10:00:00Z"),
        createdAt: new Date("2026-05-01T10:00:00Z"),
        isDeleted: false,
        author: {
          id: "user-2",
          nickname: "bob",
          avatar: null,
          plan: "pro",
        },
      },
    ]);

    const result = await listModerationQueue({ limit: 10 });

    expect(result.items).toHaveLength(2);
    // reply has newer reportedAt → first
    expect(result.items[0]!.type).toBe("comment");
    expect(result.items[0]!.id).toBe("reply-1");
    expect(result.items[0]!.postId).toBe("post-99");
    expect(result.items[1]!.type).toBe("post");
    expect(result.items[1]!.id).toBe("post-1");
    expect(result.nextCursor).toBeNull();
  });

  it("emits nextCursor when posts overflow limit", async () => {
    // limit=1, posts.length=2 → nextCursor non-null
    prismaMock.communityPost.findMany.mockResolvedValueOnce([
      {
        id: "post-1",
        userId: "user-1",
        content: "p1",
        reportedAt: new Date("2026-05-01T10:00:00Z"),
        createdAt: new Date("2026-04-30T10:00:00Z"),
        isDeleted: false,
        author: null,
      },
      {
        id: "post-2",
        userId: "user-1",
        content: "p2",
        reportedAt: new Date("2026-04-29T10:00:00Z"),
        createdAt: new Date("2026-04-29T10:00:00Z"),
        isDeleted: false,
        author: null,
      },
    ]);
    prismaMock.communityReply.findMany.mockResolvedValueOnce([]);

    const result = await listModerationQueue({ limit: 1 });

    expect(result.items).toHaveLength(1);
    expect(result.items[0]!.id).toBe("post-1");
    expect(result.nextCursor).toBe("post:post-1");
  });

  it("filters where: { isModerated: true } on both tables", async () => {
    prismaMock.communityPost.findMany.mockResolvedValueOnce([]);
    prismaMock.communityReply.findMany.mockResolvedValueOnce([]);

    await listModerationQueue();

    expect(prismaMock.communityPost.findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { isModerated: true },
      }),
    );
    expect(prismaMock.communityReply.findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { isModerated: true },
      }),
    );
  });
});

// ─── dismissModerationItem ───────────────────────────────────────────────────

describe("dismissModerationItem — flag clear", () => {
  it("sets isModerated=false on post + writes audit-log dismiss", async () => {
    prismaMock.communityPost.findUnique.mockResolvedValueOnce({
      id: "post-1",
      content: "original content",
    });
    prismaMock.communityPost.update.mockResolvedValueOnce({});
    prismaMock.moderationAction.create.mockResolvedValueOnce({});

    const result = await dismissModerationItem("post", "post-1", "admin-1");

    expect(result).toEqual({ ok: true });
    expect(prismaMock.communityPost.update).toHaveBeenCalledWith({
      where: { id: "post-1" },
      data: { isModerated: false, reportedAt: null },
    });
    expect(prismaMock.moderationAction.create).toHaveBeenCalledWith({
      data: expect.objectContaining({
        targetType: "post",
        targetId: "post-1",
        action: "dismiss",
        adminUserId: "admin-1",
        contentSnapshot: "original content",
      }),
    });
  });

  it("404 when target post not found", async () => {
    prismaMock.communityPost.findUnique.mockResolvedValueOnce(null);

    await expect(
      dismissModerationItem("post", "nonexistent", null),
    ).rejects.toMatchObject({ statusCode: 404 });

    expect(prismaMock.communityPost.update).not.toHaveBeenCalled();
    expect(prismaMock.moderationAction.create).not.toHaveBeenCalled();
  });

  it("dispatches to communityReply when type=comment", async () => {
    prismaMock.communityReply.findUnique.mockResolvedValueOnce({
      id: "reply-1",
      content: "bad reply",
    });
    prismaMock.communityReply.update.mockResolvedValueOnce({});
    prismaMock.moderationAction.create.mockResolvedValueOnce({});

    await dismissModerationItem("comment", "reply-1", null);

    expect(prismaMock.communityReply.update).toHaveBeenCalledWith({
      where: { id: "reply-1" },
      data: { isModerated: false, reportedAt: null },
    });
    expect(prismaMock.communityPost.update).not.toHaveBeenCalled();
  });
});

// ─── deleteModerationItem ────────────────────────────────────────────────────

describe("deleteModerationItem — soft-delete with audit-snapshot", () => {
  it("scrubs content + sets isDeleted=true + persists original in audit-log", async () => {
    prismaMock.communityPost.findUnique.mockResolvedValueOnce({
      id: "post-1",
      content: "this is the original toxic content",
    });
    prismaMock.communityPost.update.mockResolvedValueOnce({});
    prismaMock.moderationAction.create.mockResolvedValueOnce({});

    await deleteModerationItem("post", "post-1", "admin-1", "Hass-Rede");

    expect(prismaMock.communityPost.update).toHaveBeenCalledWith({
      where: { id: "post-1" },
      data: expect.objectContaining({
        content: "",
        isDeleted: true,
        deletedAt: expect.any(Date),
      }),
    });
    expect(prismaMock.moderationAction.create).toHaveBeenCalledWith({
      data: expect.objectContaining({
        targetType: "post",
        targetId: "post-1",
        action: "delete",
        adminUserId: "admin-1",
        contentSnapshot: "this is the original toxic content",
        reason: "Hass-Rede",
      }),
    });
  });

  it("404 when target not found", async () => {
    prismaMock.communityPost.findUnique.mockResolvedValueOnce(null);

    await expect(
      deleteModerationItem("post", "nonexistent", null),
    ).rejects.toMatchObject({ statusCode: 404 });
  });
});

// ─── banUserFromModerationItem ───────────────────────────────────────────────

describe("banUserFromModerationItem — sets Profile.banned + audit-log", () => {
  it("patches Profile.banned=true, writes audit-log ban_user", async () => {
    prismaMock.communityPost.findUnique.mockResolvedValueOnce({
      id: "post-1",
      content: "violating content",
      userId: "user-99",
    });
    prismaMock.profile.update.mockResolvedValueOnce({});
    prismaMock.moderationAction.create.mockResolvedValueOnce({});

    const result = await banUserFromModerationItem(
      "post",
      "post-1",
      "admin-1",
      "wiederholter Verstoß",
    );

    expect(result).toEqual({ ok: true, bannedUserId: "user-99" });
    expect(prismaMock.profile.update).toHaveBeenCalledWith({
      where: { id: "user-99" },
      data: expect.objectContaining({
        banned: true,
        bannedAt: expect.any(Date),
        bannedReason: "wiederholter Verstoß",
      }),
    });
    expect(prismaMock.moderationAction.create).toHaveBeenCalledWith({
      data: expect.objectContaining({
        action: "ban_user",
        contentSnapshot: "violating content",
        reason: "wiederholter Verstoß",
      }),
    });
  });

  it("uses default ban-reason when none provided", async () => {
    prismaMock.communityReply.findUnique.mockResolvedValueOnce({
      id: "reply-1",
      content: "bad",
      userId: "user-99",
    });
    prismaMock.profile.update.mockResolvedValueOnce({});
    prismaMock.moderationAction.create.mockResolvedValueOnce({});

    await banUserFromModerationItem("comment", "reply-1", null, null);

    expect(prismaMock.profile.update).toHaveBeenCalledWith({
      where: { id: "user-99" },
      data: expect.objectContaining({
        banned: true,
        bannedReason: "Moderation: comment reply-1",
      }),
    });
  });

  it("404 when target item not found", async () => {
    prismaMock.communityPost.findUnique.mockResolvedValueOnce(null);

    await expect(
      banUserFromModerationItem("post", "nonexistent", null),
    ).rejects.toMatchObject({ statusCode: 404 });

    expect(prismaMock.profile.update).not.toHaveBeenCalled();
  });
});
