import { describe, it, expect, vi, beforeEach } from "vitest";
import { initTRPC, TRPCError } from "@trpc/server";
import { wrap } from "@typeschema/valibot";
import {
  CheckNumberSchema,
  ClassifySMSSchema,
  ClassifyCallSchema,
  CreateRuleSchema,
  DeleteRuleSchema,
  FeedbackSchema,
  StatsFilterSchema,
} from "../schemas/spamshield";

vi.mock("~/server/services/spamshield.service", () => ({
  checkNumberReputation: vi.fn(),
  classifySMS: vi.fn(),
  classifyCall: vi.fn(),
  getRules: vi.fn(),
  createRule: vi.fn(),
  deleteRule: vi.fn(),
  submitFeedback: vi.fn(),
  getStats: vi.fn(),
}));

import * as spamshieldService from "~/server/services/spamshield.service";

const mockCheckNumber = vi.mocked(spamshieldService.checkNumberReputation);
const mockClassifySMS = vi.mocked(spamshieldService.classifySMS);
const mockClassifyCall = vi.mocked(spamshieldService.classifyCall);
const mockGetRules = vi.mocked(spamshieldService.getRules);
const mockCreateRule = vi.mocked(spamshieldService.createRule);
const mockDeleteRule = vi.mocked(spamshieldService.deleteRule);
const mockSubmitFeedback = vi.mocked(spamshieldService.submitFeedback);
const mockGetStats = vi.mocked(spamshieldService.getStats);

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({
    checkNumber: t.procedure
      .input(wrap(CheckNumberSchema))
      .query(async ({ input }) => {
        return mockCheckNumber(input.phoneNumber);
      }),
    classifySMS: t.procedure
      .input(wrap(ClassifySMSSchema))
      .query(async ({ input }) => {
        return mockClassifySMS(input.text);
      }),
    classifyCall: t.procedure
      .input(wrap(ClassifyCallSchema))
      .query(async ({ input }) => {
        return mockClassifyCall(input.callerNumber, input.duration, input.timeOfDay);
      }),
    getRules: t.procedure.use(isAuthed).query(async ({ ctx }) => {
      return mockGetRules(ctx.user.id);
    }),
    createRule: t.procedure.use(isAuthed)
      .input(wrap(CreateRuleSchema))
      .mutation(async ({ ctx, input }) => {
        return mockCreateRule(ctx.user.id, input.ruleType, input.pattern, input.action, input.priority);
      }),
    deleteRule: t.procedure.use(isAuthed)
      .input(wrap(DeleteRuleSchema))
      .mutation(async ({ ctx, input }) => {
        return mockDeleteRule(ctx.user.id, input.ruleId);
      }),
    submitFeedback: t.procedure.use(isAuthed)
      .input(wrap(FeedbackSchema))
      .mutation(async ({ ctx, input }) => {
        return mockSubmitFeedback(ctx.user.id, input.phoneNumber, input.isSpam, input.feedbackType);
      }),
    getStats: t.procedure.use(isAuthed)
      .input(wrap(StatsFilterSchema))
      .query(async ({ ctx, input }) => {
        return mockGetStats(ctx.user.id, input.period);
      }),
  });

  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("spamshield.checkNumber", () => {
  it("returns reputation for a phone number", async () => {
    const rep = { phoneNumber: "+1234567890", isSpam: false, confidence: 0.95, source: "internal", score: 0, category: "unknown" };
    mockCheckNumber.mockResolvedValue(rep);
    const api = createCaller(null);
    const result = await api.checkNumber({ phoneNumber: "+1234567890" });
    expect(result).toEqual(rep);
  });

  it("normalizes phone number", async () => {
    mockCheckNumber.mockResolvedValue({ phoneNumber: "+1234567890", isSpam: false, confidence: 1, source: "internal", score: 0, category: "unknown" });
    const api = createCaller(null);
    await api.checkNumber({ phoneNumber: "234567890" });
    expect(mockCheckNumber).toHaveBeenCalledWith("234567890");
  });
});

describe("spamshield.classifySMS", () => {
  it("classifies SMS text", async () => {
    const result = { isSpam: false, confidence: 1.0, score: 0.0 };
    mockClassifySMS.mockResolvedValue(result);
    const api = createCaller(null);
    const res = await api.classifySMS({ text: "Hello friend" });
    expect(res.isSpam).toBe(false);
  });
});

describe("spamshield.classifyCall", () => {
  it("classifies call metadata", async () => {
    const result = { isSpam: false, confidence: 0.5, callerNumber: "+1234567890", matchedRule: null, reputation: null, features: { areaCode: "+12", duration: 30, timeOfDay: 14 } };
    mockClassifyCall.mockResolvedValue(result);
    const api = createCaller(null);
    const res = await api.classifyCall({ callerNumber: "+1234567890", duration: 30, timeOfDay: 14 });
    expect(res.isSpam).toBe(false);
  });
});

describe("spamshield.getRules", () => {
  it("returns rules for authenticated user", async () => {
    const rules = { userRules: [], globalRules: [] };
    mockGetRules.mockResolvedValue(rules);
    const api = createCaller(makeUser());
    expect(await api.getRules()).toEqual(rules);
  });

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

describe("spamshield.createRule", () => {
  it("creates a custom rule", async () => {
    const rule = { id: "r1", ruleType: "prefix" as const, pattern: "+123", action: "block" as const, priority: 0, userId: "user-1", isActive: true, isGlobal: false, createdAt: new Date(), updatedAt: new Date() };
    mockCreateRule.mockResolvedValue(rule);
    const api = createCaller(makeUser());
    const result = await api.createRule({ ruleType: "prefix", pattern: "+123", action: "block" });
    expect(result.id).toBe("r1");
  });

  it("rejects invalid rule type", async () => {
    const api = createCaller(makeUser());
    await expect(
      api.createRule({ ruleType: "invalid" as never, pattern: "test", action: "block" }),
    ).rejects.toThrow();
  });
});

describe("spamshield.deleteRule", () => {
  it("deletes a rule", async () => {
    mockDeleteRule.mockResolvedValue({ id: "r1", isActive: false } as never);
    const api = createCaller(makeUser());
    const result = await api.deleteRule({ ruleId: "r1" });
    expect(result.isActive).toBe(false);
  });
});

describe("spamshield.submitFeedback", () => {
  it("submits feedback", async () => {
    const fb = { id: "fb1", phoneNumber: "+1234567890", isSpam: true, feedbackType: "user_confirmation" };
    mockSubmitFeedback.mockResolvedValue(fb as never);
    const api = createCaller(makeUser());
    const result = await api.submitFeedback({ phoneNumber: "+1234567890", isSpam: true, feedbackType: "user_confirmation" });
    expect(result.isSpam).toBe(true);
  });

  it("rejects invalid feedback type", async () => {
    const api = createCaller(makeUser());
    await expect(
      api.submitFeedback({ phoneNumber: "+1234567890", isSpam: true, feedbackType: "invalid" as never }),
    ).rejects.toThrow();
  });
});

describe("spamshield.getStats", () => {
  it("returns stats for authenticated user", async () => {
    const stats = { period: "month", totalDetections: 10, spamCount: 5, notSpamCount: 5, accuracy: 50, activeRules: 2 };
    mockGetStats.mockResolvedValue(stats);
    const api = createCaller(makeUser());
    const result = await api.getStats({ period: "month" });
    expect(result.accuracy).toBe(50);
  });

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