import { describe, it, expect, vi, beforeEach } from "vitest";
import { initTRPC, TRPCError } from "@trpc/server";
import { wrap } from "@typeschema/valibot";
import {
  AddWatchlistItemSchema,
  RemoveWatchlistItemSchema,
  ExposureFilterSchema,
  ExposureDetailsSchema,
  RunScanSchema,
  ReportFilterSchema,
} from "../schemas/darkwatch";

vi.mock("~/server/services/darkwatch.service", () => ({
  getWatchlistItems: vi.fn(),
  addWatchlistItem: vi.fn(),
  removeWatchlistItem: vi.fn(),
  getExposures: vi.fn(),
  getExposureDetails: vi.fn(),
  runScan: vi.fn(),
  getScanStatus: vi.fn(),
  getReports: vi.fn(),
}));

import * as darkwatchService from "~/server/services/darkwatch.service";

const mockGetWatchlistItems = vi.mocked(darkwatchService.getWatchlistItems);
const mockAddWatchlistItem = vi.mocked(darkwatchService.addWatchlistItem);
const mockRemoveWatchlistItem = vi.mocked(darkwatchService.removeWatchlistItem);
const mockGetExposures = vi.mocked(darkwatchService.getExposures);
const mockGetExposureDetails = vi.mocked(darkwatchService.getExposureDetails);
const mockRunScan = vi.mocked(darkwatchService.runScan);
const mockGetScanStatus = vi.mocked(darkwatchService.getScanStatus);
const mockGetReports = vi.mocked(darkwatchService.getReports);

type User = {
  id: string; email: string; name: string | null; image: string | null;
  role: string; emailVerified: Date | null; deletedAt: Date | null;
  stripeCustomerId: string | null;
  createdAt: Date; updatedAt: Date;
};
type Ctx = { db: object; user: User | null; apiKey: string | null };

function createCaller(user: User | null) {
  const t = initTRPC.context<Ctx>().create();
  const isAuthed = t.middleware(({ ctx, next }) => {
    if (!ctx.user) throw new TRPCError({ code: "UNAUTHORIZED" });
    return next({ ctx: { ...ctx, user: ctx.user } });
  });

  const router = t.router({
    getWatchlist: t.procedure.use(isAuthed).query(async ({ ctx }) => {
      return mockGetWatchlistItems(ctx.user.id);
    }),
    addWatchlistItem: t.procedure.use(isAuthed)
      .input(wrap(AddWatchlistItemSchema))
      .mutation(async ({ ctx, input }) => {
        return mockAddWatchlistItem(ctx.user.id, input.type, input.value);
      }),
    removeWatchlistItem: t.procedure.use(isAuthed)
      .input(wrap(RemoveWatchlistItemSchema))
      .mutation(async ({ ctx, input }) => {
        return mockRemoveWatchlistItem(ctx.user.id, input.itemId);
      }),
    getExposures: t.procedure.use(isAuthed)
      .input(wrap(ExposureFilterSchema))
      .query(async ({ ctx, input }) => {
        return mockGetExposures(ctx.user.id, input);
      }),
    getExposureDetails: t.procedure.use(isAuthed)
      .input(wrap(ExposureDetailsSchema))
      .query(async ({ ctx, input }) => {
        return mockGetExposureDetails(ctx.user.id, input.exposureId);
      }),
    runScan: t.procedure.use(isAuthed)
      .input(wrap(RunScanSchema))
      .mutation(async ({ ctx }) => {
        return mockRunScan(ctx.user.id);
      }),
    getScanStatus: t.procedure.use(isAuthed).query(async ({ ctx }) => {
      return mockGetScanStatus(ctx.user.id);
    }),
    getReports: t.procedure.use(isAuthed)
      .input(wrap(ReportFilterSchema))
      .query(async ({ ctx, input }) => {
        return mockGetReports(ctx.user.id, input);
      }),
  });

  const caller = t.createCallerFactory(router);
  return caller({ db: {} as never, user, apiKey: null });
}

const baseUser: User = {
  id: "user-1", email: "a@b.com", name: "Test", image: null,
  role: "user", emailVerified: null, deletedAt: null,
  stripeCustomerId: null,
  createdAt: new Date(), updatedAt: new Date(),
};

function makeUser(overrides: Partial<User> = {}): User {
  return { ...baseUser, ...overrides };
}

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

describe("darkwatch.getWatchlist", () => {
  it("returns watchlist items for authenticated user", async () => {
    const items = [{ id: "w1", type: "email", value: "test@example.com" }];
    mockGetWatchlistItems.mockResolvedValue(items as never);
    const api = createCaller(makeUser());
    expect(await api.getWatchlist()).toEqual(items);
  });

  it("rejects unauthenticated", async () => {
    const api = createCaller(null);
    await expect(api.getWatchlist()).rejects.toThrow(TRPCError);
  });
});

describe("darkwatch.addWatchlistItem", () => {
  it("adds a watchlist item", async () => {
    const item = { id: "w1", type: "email", value: "test@example.com" };
    mockAddWatchlistItem.mockResolvedValue(item as never);
    const api = createCaller(makeUser());
    const result = await api.addWatchlistItem({ type: "email", value: "test@example.com" });
    expect(result).toEqual(item);
  });

  it("rejects invalid type", async () => {
    const api = createCaller(makeUser());
    await expect(
      api.addWatchlistItem({ type: "invalid" as never, value: "test" }),
    ).rejects.toThrow();
  });
});

describe("darkwatch.removeWatchlistItem", () => {
  it("removes a watchlist item", async () => {
    mockRemoveWatchlistItem.mockResolvedValue({ id: "w1", isActive: false } as never);
    const api = createCaller(makeUser());
    const result = await api.removeWatchlistItem({ itemId: "w1" });
    expect(result.isActive).toBe(false);
  });
});

describe("darkwatch.getExposures", () => {
  it("returns exposures with pagination", async () => {
    const data = { items: [], total: 0, page: 1, limit: 20, totalPages: 0 };
    mockGetExposures.mockResolvedValue(data);
    const api = createCaller(makeUser());
    const result = await api.getExposures({ page: 1, limit: 20 });
    expect(result.total).toBe(0);
  });

  it("passes severity filter", async () => {
    const data = { items: [], total: 0, page: 1, limit: 20, totalPages: 0 };
    mockGetExposures.mockResolvedValue(data);
    const api = createCaller(makeUser());
    await api.getExposures({ severity: "critical" });
    expect(mockGetExposures).toHaveBeenCalledWith("user-1", { severity: "critical", page: 1, limit: 20 });
  });
});

describe("darkwatch.getExposureDetails", () => {
  it("returns exposure details", async () => {
    const exposure = { id: "e1", identifier: "test@example.com", watchlistItem: null };
    mockGetExposureDetails.mockResolvedValue(exposure as never);
    const api = createCaller(makeUser());
    const result = await api.getExposureDetails({ exposureId: "e1" });
    expect(result.id).toBe("e1");
  });
});

describe("darkwatch.runScan", () => {
  it("triggers a scan", async () => {
    mockRunScan.mockResolvedValue({ scanId: "s1" });
    const api = createCaller(makeUser());
    const result = await api.runScan({});
    expect(result.scanId).toBe("s1");
  });
});

describe("darkwatch.getScanStatus", () => {
  it("returns scan status", async () => {
    mockGetScanStatus.mockResolvedValue({ status: "idle", startedAt: null, completedAt: null, progress: 0, error: null });
    const api = createCaller(makeUser());
    const result = await api.getScanStatus();
    expect(result.status).toBe("idle");
  });
});

describe("darkwatch.getReports", () => {
  it("returns reports", async () => {
    const data = { items: [], total: 0, page: 1, limit: 20, totalPages: 0 };
    mockGetReports.mockResolvedValue(data);
    const api = createCaller(makeUser());
    const result = await api.getReports({ page: 1, limit: 20 });
    expect(result.total).toBe(0);
  });
});
