import { describe, it, expect, vi, beforeEach } from "vitest";
import { NextRequest } from "next/server";

// ── Hoisted mocks ─────────────────────────────────────────────────────────────
const {
  mockGetServerSession,
  mockDbSelect,
  mockDbInsert,
  mockDbDelete,
} = vi.hoisted(() => ({
  mockGetServerSession: vi.fn(),
  mockDbSelect: vi.fn(),
  mockDbInsert: vi.fn(),
  mockDbDelete: vi.fn(),
}));

vi.mock("next-auth", () => ({ getServerSession: mockGetServerSession }));
vi.mock("@/app/api/auth/[...nextauth]/authOptions", () => ({ AuthOptions: {} }));
vi.mock("drizzle-orm", () => ({
  and: vi.fn((...args: unknown[]) => ({ $and: args })),
  eq: vi.fn((a: unknown, b: unknown) => ({ $eq: [a, b] })),
  inArray: vi.fn((col: unknown, vals: unknown) => ({ $inArray: [col, vals] })),
}));
vi.mock("@/app/db/schema/portfolio_organisation", () => ({
  portfolioOrganisation: {
    portfolioId: {},
    organisationId: {},
    id: {},
  },
}));
vi.mock("@/app/db/schema/organisation", () => ({
  organisation: { id: {}, name: {}, hubspotCompanyId: {} },
}));
vi.mock("@/app/db/db", () => ({
  db: {
    get select() { return mockDbSelect; },
    get insert() { return mockDbInsert; },
    get delete() { return mockDbDelete; },
  },
}));

// ── Chain builders ────────────────────────────────────────────────────────────
function makeSelectChain(rows: unknown[]) {
  const self: Record<string, unknown> = {};
  self["then"] = (resolve: (v: unknown) => unknown, reject: (e: unknown) => unknown) =>
    Promise.resolve(rows).then(resolve, reject);
  self["from"] = vi.fn(() => self);
  self["innerJoin"] = vi.fn(() => self);
  self["leftJoin"] = vi.fn(() => self);
  self["where"] = vi.fn(() => self);
  self["limit"] = vi.fn(() => Promise.resolve(rows));
  return self;
}

function makeInsertChain() {
  const self: Record<string, unknown> = {};
  self["values"] = vi.fn(() => Promise.resolve([]));
  return self;
}

function makeDeleteChain() {
  const self: Record<string, unknown> = {};
  self["where"] = vi.fn(() => Promise.resolve([]));
  return self;
}

function makeParams(portfolioId = "42") {
  return Promise.resolve({ portfolioId });
}

function makeRequest(method: string, body?: unknown, portfolioId = "42") {
  return new NextRequest(
    `http://localhost/api/portfolio/${portfolioId}/organisation`,
    {
      method,
      ...(body ? { body: JSON.stringify(body), headers: { "content-type": "application/json" } } : {}),
    },
  );
}

// ── Subject under test ────────────────────────────────────────────────────────
import { GET, POST, DELETE } from "./route";

describe("GET /portfolio/:id/organisation", () => {
  beforeEach(() => vi.clearAllMocks());

  it("returns empty array when no orgs linked", async () => {
    mockDbSelect.mockImplementationOnce(() => makeSelectChain([]));
    const res = await GET(makeRequest("GET"), { params: makeParams() });
    expect(res.status).toBe(200);
    const json = await res.json();
    expect(json).toEqual([]);
  });

  it("returns all linked orgs as array", async () => {
    const orgs = [
      { id: "org-1", name: "Alpha Housing", hubspotCompanyId: "hs-1" },
      { id: "org-2", name: "Beta Council", hubspotCompanyId: "hs-2" },
    ];
    mockDbSelect.mockImplementationOnce(() => makeSelectChain(orgs));
    const res = await GET(makeRequest("GET"), { params: makeParams() });
    expect(res.status).toBe(200);
    const json = await res.json();
    expect(json).toHaveLength(2);
    expect(json[0].name).toBe("Alpha Housing");
    expect(json[1].name).toBe("Beta Council");
  });
});

describe("POST /portfolio/:id/organisation", () => {
  beforeEach(() => vi.clearAllMocks());

  it("returns 403 for non-Domna user", async () => {
    mockGetServerSession.mockResolvedValue({ user: { email: "outsider@other.com" } });
    const res = await POST(makeRequest("POST", { organisationId: "org-1" }), { params: makeParams() });
    expect(res.status).toBe(403);
  });

  it("returns 400 when organisationId missing", async () => {
    mockGetServerSession.mockResolvedValue({ user: { email: "admin@domna.homes" } });
    const res = await POST(makeRequest("POST", {}), { params: makeParams() });
    expect(res.status).toBe(400);
  });

  it("returns 409 when org already linked to this portfolio", async () => {
    mockGetServerSession.mockResolvedValue({ user: { email: "admin@domna.homes" } });
    // existing link found
    mockDbSelect.mockImplementationOnce(() => makeSelectChain([{ id: "link-1" }]));
    const res = await POST(makeRequest("POST", { organisationId: "org-1" }), { params: makeParams() });
    expect(res.status).toBe(409);
  });

  it("adds org without removing existing links", async () => {
    mockGetServerSession.mockResolvedValue({ user: { email: "admin@domna.homes" } });
    // no existing link for this org
    mockDbSelect.mockImplementationOnce(() => makeSelectChain([]));
    const insertChain = makeInsertChain();
    mockDbInsert.mockImplementationOnce(() => insertChain);
    // return updated list
    mockDbSelect.mockImplementationOnce(() =>
      makeSelectChain([
        { id: "org-1", name: "Alpha Housing", hubspotCompanyId: "hs-1" },
        { id: "org-2", name: "Beta Council", hubspotCompanyId: "hs-2" },
      ]),
    );
    const res = await POST(makeRequest("POST", { organisationId: "org-2" }), { params: makeParams() });
    expect(res.status).toBe(200);
    // insert called — no delete called
    expect(mockDbDelete).not.toHaveBeenCalled();
    expect(insertChain.values).toHaveBeenCalled();
    const json = await res.json();
    expect(json).toHaveLength(2);
  });
});

describe("DELETE /portfolio/:id/organisation", () => {
  beforeEach(() => vi.clearAllMocks());

  it("returns 403 for non-Domna user", async () => {
    mockGetServerSession.mockResolvedValue({ user: { email: "outsider@other.com" } });
    const res = await DELETE(makeRequest("DELETE", { organisationId: "org-1" }), { params: makeParams() });
    expect(res.status).toBe(403);
  });

  it("removes the specific org link", async () => {
    mockGetServerSession.mockResolvedValue({ user: { email: "admin@domna.homes" } });
    const deleteChain = makeDeleteChain();
    mockDbDelete.mockImplementationOnce(() => deleteChain);
    const res = await DELETE(makeRequest("DELETE", { organisationId: "org-1" }), { params: makeParams() });
    expect(res.status).toBe(200);
    expect(deleteChain.where).toHaveBeenCalled();
    const json = await res.json();
    expect(json.success).toBe(true);
  });

  it("returns 400 when organisationId missing", async () => {
    mockGetServerSession.mockResolvedValue({ user: { email: "admin@domna.homes" } });
    const res = await DELETE(makeRequest("DELETE", {}), { params: makeParams() });
    expect(res.status).toBe(400);
  });
});
