import { describe, it, expect, vi, beforeEach } from "vitest";
import { initTRPC, TRPCError } from "@trpc/server";
import { wrap } from "@typeschema/valibot";
import {
  AddPropertySchema,
  RemovePropertySchema,
  GetSnapshotsSchema,
  GetChangesSchema,
  RunScanSchema,
} from "../schemas/hometitle";

vi.mock("~/server/services/hometitle.service", () => ({
  getProperties: vi.fn(),
  addProperty: vi.fn(),
  removeProperty: vi.fn(),
  getSnapshots: vi.fn(),
  getChanges: vi.fn(),
  runScan: vi.fn(),
  getAlerts: vi.fn(),
}));

import * as hometitleService from "~/server/services/hometitle.service";

const mockGetProperties = vi.mocked(hometitleService.getProperties);
const mockAddProperty = vi.mocked(hometitleService.addProperty);
const mockRemoveProperty = vi.mocked(hometitleService.removeProperty);
const mockGetSnapshots = vi.mocked(hometitleService.getSnapshots);
const mockGetChanges = vi.mocked(hometitleService.getChanges);
const mockRunScan = vi.mocked(hometitleService.runScan);
const mockGetAlerts = vi.mocked(hometitleService.getAlerts);

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({
    getProperties: t.procedure.use(isAuthed).query(async ({ ctx }) => {
      return mockGetProperties(ctx.user.id);
    }),
    addProperty: t.procedure.use(isAuthed)
      .input(wrap(AddPropertySchema))
      .mutation(async ({ ctx, input }) => {
        return mockAddProperty(ctx.user.id, input.address, input.parcelId, input.ownerName);
      }),
    removeProperty: t.procedure.use(isAuthed)
      .input(wrap(RemovePropertySchema))
      .mutation(async ({ ctx, input }) => {
        return mockRemoveProperty(ctx.user.id, input.propertyId);
      }),
    getSnapshots: t.procedure.use(isAuthed)
      .input(wrap(GetSnapshotsSchema))
      .query(async ({ ctx, input }) => {
        return mockGetSnapshots(ctx.user.id, input.propertyId);
      }),
    getChanges: t.procedure.use(isAuthed)
      .input(wrap(GetChangesSchema))
      .query(async ({ ctx, input }) => {
        return mockGetChanges(ctx.user.id, input.propertyId, {
          severity: input.severity,
          changeType: input.changeType,
        });
      }),
    runScan: t.procedure.use(isAuthed)
      .input(wrap(RunScanSchema))
      .mutation(async ({ ctx }) => {
        return mockRunScan(ctx.user.id);
      }),
    getAlerts: t.procedure.use(isAuthed).query(async ({ ctx }) => {
      return mockGetAlerts(ctx.user.id);
    }),
  });

  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("hometitle.getProperties", () => {
  it("returns properties for authenticated user", async () => {
    const items = [{ id: "p1", address: "123 Main St" }];
    mockGetProperties.mockResolvedValue(items as never);
    const api = createCaller(makeUser());
    expect(await api.getProperties()).toEqual(items);
  });

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

describe("hometitle.addProperty", () => {
  it("adds a property", async () => {
    const prop = { id: "p1", address: "123 Main St, Springfield, IL 62701" };
    mockAddProperty.mockResolvedValue(prop as never);
    const api = createCaller(makeUser());
    const result = await api.addProperty({ address: "123 Main St, Springfield, IL 62701" });
    expect(result).toEqual(prop);
  });

  it("rejects empty address", async () => {
    const api = createCaller(makeUser());
    await expect(
      api.addProperty({ address: "" }),
    ).rejects.toThrow();
  });
});

describe("hometitle.removeProperty", () => {
  it("removes a property", async () => {
    mockRemoveProperty.mockResolvedValue({ id: "p1", isActive: false } as never);
    const api = createCaller(makeUser());
    const result = await api.removeProperty({ propertyId: "p1" });
    expect(result.isActive).toBe(false);
  });
});

describe("hometitle.getSnapshots", () => {
  it("returns snapshots for a property", async () => {
    const snapshots = [{ id: "s1", ownerName: "John Doe" }];
    mockGetSnapshots.mockResolvedValue(snapshots as never);
    const api = createCaller(makeUser());
    const result = await api.getSnapshots({ propertyId: "p1" });
    expect(result).toEqual(snapshots);
  });
});

describe("hometitle.getChanges", () => {
  it("returns changes for a property", async () => {
    const changes = [{ id: "c1", changeType: "ownership_transfer" }];
    mockGetChanges.mockResolvedValue(changes as never);
    const api = createCaller(makeUser());
    const result = await api.getChanges({ propertyId: "p1" });
    expect(result).toEqual(changes);
  });

  it("passes severity filter", async () => {
    const changes = [{ id: "c1", severity: "critical" }];
    mockGetChanges.mockResolvedValue(changes as never);
    const api = createCaller(makeUser());
    await api.getChanges({ propertyId: "p1", severity: "critical" });
    expect(mockGetChanges).toHaveBeenCalledWith("user-1", "p1", { severity: "critical", changeType: undefined });
  });
});

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

describe("hometitle.getAlerts", () => {
  it("returns alerts", async () => {
    const alerts = [{ id: "a1", propertyAddress: "123 Main St" }];
    mockGetAlerts.mockResolvedValue(alerts as never);
    const api = createCaller(makeUser());
    const result = await api.getAlerts();
    expect(result).toEqual(alerts);
  });
});
