import { describe, expect, it, vi, beforeEach } from "vitest";
import { appRouter } from "./routers";
import type { TrpcContext } from "./_core/context";

type AuthenticatedUser = NonNullable<TrpcContext["user"]>;

/* ─── Context helpers ─── */
function createContext(role: "admin" | "agent" | "user" | null): TrpcContext {
  if (!role) {
    return {
      user: null,
      req: { protocol: "https", headers: {} } as TrpcContext["req"],
      res: { clearCookie: vi.fn() } as unknown as TrpcContext["res"],
    };
  }

  const user: AuthenticatedUser = {
    id: role === "admin" ? 1 : role === "agent" ? 2 : 3,
    openId: `${role}-openid`,
    email: `${role}@homelegance.com`,
    name: `Test ${role.charAt(0).toUpperCase() + role.slice(1)}`,
    loginMethod: "manus",
    role,
    createdAt: new Date(),
    updatedAt: new Date(),
    lastSignedIn: new Date(),
  };

  return {
    user,
    req: { protocol: "https", headers: {} } as TrpcContext["req"],
    res: { clearCookie: vi.fn() } as unknown as TrpcContext["res"],
  };
}

/* ─── Mock database ─── */
const mockUsers = [
  { id: 1, openId: "admin-openid", name: "Test Admin", email: "admin@homelegance.com", role: "admin", lastSignedIn: new Date(), createdAt: new Date(), updatedAt: new Date() },
  { id: 2, openId: "agent-openid", name: "Test Agent", email: "agent@homelegance.com", role: "agent", lastSignedIn: new Date(), createdAt: new Date(), updatedAt: new Date() },
  { id: 3, openId: "user-openid", name: "Test User", email: "user@homelegance.com", role: "user", lastSignedIn: new Date(), createdAt: new Date(), updatedAt: new Date() },
];

const mockInvitations: any[] = [];
const mockAuditLogs: any[] = [];
let inviteIdCounter = 1;
let auditIdCounter = 1;

vi.mock("./db", () => ({
  createConversation: vi.fn(async (data: any) => ({ id: 1, ...data, createdAt: new Date(), updatedAt: new Date() })),
  getConversations: vi.fn(async () => []),
  getConversationById: vi.fn(async (id: number) => ({ id, sessionId: "test", status: "active", createdAt: new Date(), updatedAt: new Date() })),
  getConversationBySessionId: vi.fn(async () => null),
  updateConversationStatus: vi.fn(async (id: number, status: string) => ({ id, status })),
  getConversationStats: vi.fn(async () => ({ total: 5, active: 2, escalated: 1, resolved: 1, closed: 1 })),
  addMessage: vi.fn(async (data: any) => ({ id: 1, ...data, createdAt: new Date() })),
  getMessagesByConversation: vi.fn(async () => []),
  saveWorkflow: vi.fn(async (wid: string, nodes: any[], edges: any[]) => ({ workflowId: wid, nodeCount: nodes.length, edgeCount: edges.length })),
  getWorkflow: vi.fn(async () => ({ nodes: [], edges: [] })),
  getAllUsers: vi.fn(async () => [...mockUsers]),
  updateUserRole: vi.fn(async (userId: number, role: string) => {
    const u = mockUsers.find(u => u.id === userId);
    if (!u) return null;
    return { ...u, role };
  }),
  getUserById: vi.fn(async (userId: number) => mockUsers.find(u => u.id === userId) || null),
  getUserByEmail: vi.fn(async (email: string) => mockUsers.find(u => u.email === email) || null),
  deleteUser: vi.fn(async (userId: number) => {
    const u = mockUsers.find(u => u.id === userId);
    return u || null;
  }),
  createInvitation: vi.fn(async (data: any) => {
    const inv = { id: inviteIdCounter++, ...data, createdAt: new Date() };
    mockInvitations.push(inv);
    return inv;
  }),
  getAllInvitations: vi.fn(async () => [...mockInvitations]),
  getInvitationByToken: vi.fn(async (token: string) => mockInvitations.find(i => i.token === token) || null),
  getInvitationByEmail: vi.fn(async (email: string) => mockInvitations.filter(i => i.email === email)),
  updateInvitationStatus: vi.fn(async (id: number, status: string, acceptedById?: number) => {
    const inv = mockInvitations.find(i => i.id === id);
    if (inv) {
      inv.status = status;
      if (acceptedById) inv.acceptedById = acceptedById;
      inv.acceptedAt = status === "accepted" ? new Date() : null;
    }
    return inv;
  }),
  expireOldInvitations: vi.fn(async () => {}),
  createAuditLog: vi.fn(async (data: any) => {
    const log = { id: auditIdCounter++, ...data, createdAt: new Date() };
    mockAuditLogs.push(log);
    return log;
  }),
  getAuditLogs: vi.fn(async (limit?: number) => mockAuditLogs.slice(0, limit || 50)),
  upsertUser: vi.fn(),
  getUserByOpenId: vi.fn(),
}));

vi.mock("./_core/llm", () => ({
  invokeLLM: vi.fn(async () => ({
    id: "test",
    created: Date.now(),
    model: "test",
    choices: [{ index: 0, message: { role: "assistant", content: "Test response" }, finish_reason: "stop" }],
  })),
}));

vi.mock("./_core/notification", () => ({
  notifyOwner: vi.fn(async () => true),
}));

/* ═══════════════════════════════════════════════════════════════
   INVITATION SYSTEM
   ═══════════════════════════════════════════════════════════════ */

describe("Invitation system", () => {
  beforeEach(() => {
    mockInvitations.length = 0;
    mockAuditLogs.length = 0;
    inviteIdCounter = 1;
    auditIdCounter = 1;
  });

  describe("Send invitation", () => {
    it("admin can send an invitation", async () => {
      const caller = appRouter.createCaller(createContext("admin"));
      const result = await caller.invitations.send({
        email: "newuser@example.com",
        role: "agent",
        message: "Welcome to the team!",
      });
      expect(result).toHaveProperty("email", "newuser@example.com");
      expect(result).toHaveProperty("role", "agent");
      expect(result).toHaveProperty("token");
      expect(result).toHaveProperty("status", "pending");
    });

    it("rejects invitation for existing user email", async () => {
      const caller = appRouter.createCaller(createContext("admin"));
      await expect(
        caller.invitations.send({ email: "agent@homelegance.com", role: "admin" })
      ).rejects.toThrow("already exists");
    });

    it("rejects duplicate pending invitation", async () => {
      const caller = appRouter.createCaller(createContext("admin"));
      await caller.invitations.send({ email: "dup@example.com", role: "agent" });
      await expect(
        caller.invitations.send({ email: "dup@example.com", role: "admin" })
      ).rejects.toThrow("pending invitation already exists");
    });

    it("agent cannot send invitations", async () => {
      const caller = appRouter.createCaller(createContext("agent"));
      await expect(
        caller.invitations.send({ email: "test@example.com", role: "agent" })
      ).rejects.toThrow();
    });

    it("regular user cannot send invitations", async () => {
      const caller = appRouter.createCaller(createContext("user"));
      await expect(
        caller.invitations.send({ email: "test@example.com", role: "agent" })
      ).rejects.toThrow();
    });
  });

  describe("List invitations", () => {
    it("admin can list invitations", async () => {
      const caller = appRouter.createCaller(createContext("admin"));
      await caller.invitations.send({ email: "list@example.com", role: "agent" });
      const result = await caller.invitations.list();
      expect(Array.isArray(result)).toBe(true);
      expect(result.length).toBeGreaterThan(0);
    });

    it("agent cannot list invitations", async () => {
      const caller = appRouter.createCaller(createContext("agent"));
      await expect(caller.invitations.list()).rejects.toThrow();
    });
  });

  describe("Revoke invitation", () => {
    it("admin can revoke a pending invitation", async () => {
      const caller = appRouter.createCaller(createContext("admin"));
      const inv = await caller.invitations.send({ email: "revoke@example.com", role: "agent" });
      const result = await caller.invitations.revoke({ invitationId: inv.id });
      expect(result).toHaveProperty("status", "revoked");
    });

    it("cannot revoke a non-pending invitation", async () => {
      const caller = appRouter.createCaller(createContext("admin"));
      const inv = await caller.invitations.send({ email: "revoke2@example.com", role: "agent" });
      await caller.invitations.revoke({ invitationId: inv.id });
      await expect(
        caller.invitations.revoke({ invitationId: inv.id })
      ).rejects.toThrow("Cannot revoke");
    });
  });

  describe("Validate invitation", () => {
    it("validates a pending invitation", async () => {
      const caller = appRouter.createCaller(createContext("admin"));
      const inv = await caller.invitations.send({ email: "validate@example.com", role: "agent" });

      const publicCaller = appRouter.createCaller(createContext(null));
      const result = await publicCaller.invitations.validate({ token: inv.token });
      expect(result.valid).toBe(true);
      if (result.valid) {
        expect(result.email).toBe("validate@example.com");
        expect(result.role).toBe("agent");
      }
    });

    it("returns invalid for non-existent token", async () => {
      const publicCaller = appRouter.createCaller(createContext(null));
      const result = await publicCaller.invitations.validate({ token: "nonexistent" });
      expect(result.valid).toBe(false);
    });

    it("returns invalid for revoked invitation", async () => {
      const caller = appRouter.createCaller(createContext("admin"));
      const inv = await caller.invitations.send({ email: "revoked@example.com", role: "agent" });
      await caller.invitations.revoke({ invitationId: inv.id });

      const publicCaller = appRouter.createCaller(createContext(null));
      const result = await publicCaller.invitations.validate({ token: inv.token });
      expect(result.valid).toBe(false);
    });
  });

  describe("Accept invitation", () => {
    it("authenticated user can accept a valid invitation", async () => {
      const adminCaller = appRouter.createCaller(createContext("admin"));
      const inv = await adminCaller.invitations.send({ email: "accept@example.com", role: "agent" });

      const userCaller = appRouter.createCaller(createContext("user"));
      const result = await userCaller.invitations.accept({ token: inv.token });
      expect(result.success).toBe(true);
      expect(result.role).toBe("agent");
    });

    it("unauthenticated user cannot accept invitation", async () => {
      const adminCaller = appRouter.createCaller(createContext("admin"));
      const inv = await adminCaller.invitations.send({ email: "noauth@example.com", role: "agent" });

      const publicCaller = appRouter.createCaller(createContext(null));
      await expect(
        publicCaller.invitations.accept({ token: inv.token })
      ).rejects.toThrow();
    });
  });

  describe("Resend invitation", () => {
    it("admin can resend a pending invitation", async () => {
      const caller = appRouter.createCaller(createContext("admin"));
      const inv = await caller.invitations.send({ email: "resend@example.com", role: "agent" });
      const result = await caller.invitations.resend({ invitationId: inv.id });
      expect(result).toHaveProperty("email", "resend@example.com");
      expect(result).toHaveProperty("status", "pending");
      expect(result.token).not.toBe(inv.token); // New token
    });
  });
});

/* ═══════════════════════════════════════════════════════════════
   USER DELETION
   ═══════════════════════════════════════════════════════════════ */

describe("User deletion", () => {
  beforeEach(() => {
    mockAuditLogs.length = 0;
    auditIdCounter = 1;
  });

  it("admin can delete a user", async () => {
    const caller = appRouter.createCaller(createContext("admin"));
    const result = await caller.users.delete({ userId: 2 });
    expect(result.success).toBe(true);
  });

  it("admin cannot delete themselves", async () => {
    const caller = appRouter.createCaller(createContext("admin"));
    await expect(
      caller.users.delete({ userId: 1 })
    ).rejects.toThrow("You cannot delete your own account");
  });

  it("agent cannot delete users", async () => {
    const caller = appRouter.createCaller(createContext("agent"));
    await expect(
      caller.users.delete({ userId: 3 })
    ).rejects.toThrow();
  });

  it("regular user cannot delete users", async () => {
    const caller = appRouter.createCaller(createContext("user"));
    await expect(
      caller.users.delete({ userId: 2 })
    ).rejects.toThrow();
  });
});

/* ═══════════════════════════════════════════════════════════════
   BULK ACTIONS
   ═══════════════════════════════════════════════════════════════ */

describe("Bulk actions", () => {
  beforeEach(() => {
    mockAuditLogs.length = 0;
    auditIdCounter = 1;
  });

  it("admin can bulk update roles", async () => {
    const caller = appRouter.createCaller(createContext("admin"));
    const results = await caller.users.bulkUpdateRole({ userIds: [2, 3], role: "agent" });
    expect(results).toHaveLength(2);
    expect(results.every(r => r.success)).toBe(true);
  });

  it("bulk update skips self", async () => {
    const caller = appRouter.createCaller(createContext("admin"));
    const results = await caller.users.bulkUpdateRole({ userIds: [1, 2], role: "user" });
    expect(results[0].success).toBe(false);
    expect(results[0].error).toBe("Cannot change own role");
    expect(results[1].success).toBe(true);
  });

  it("admin can bulk delete users", async () => {
    const caller = appRouter.createCaller(createContext("admin"));
    const results = await caller.users.bulkDelete({ userIds: [2, 3] });
    expect(results).toHaveLength(2);
    expect(results.every(r => r.success)).toBe(true);
  });

  it("bulk delete skips self", async () => {
    const caller = appRouter.createCaller(createContext("admin"));
    const results = await caller.users.bulkDelete({ userIds: [1, 2] });
    expect(results[0].success).toBe(false);
    expect(results[0].error).toBe("Cannot delete own account");
    expect(results[1].success).toBe(true);
  });

  it("agent cannot perform bulk actions", async () => {
    const caller = appRouter.createCaller(createContext("agent"));
    await expect(
      caller.users.bulkUpdateRole({ userIds: [3], role: "agent" })
    ).rejects.toThrow();
    await expect(
      caller.users.bulkDelete({ userIds: [3] })
    ).rejects.toThrow();
  });
});

/* ═══════════════════════════════════════════════════════════════
   AUDIT LOGS
   ═══════════════════════════════════════════════════════════════ */

describe("Audit logs", () => {
  beforeEach(() => {
    mockAuditLogs.length = 0;
    auditIdCounter = 1;
  });

  it("admin can view audit logs", async () => {
    const caller = appRouter.createCaller(createContext("admin"));
    const result = await caller.auditLogs.list({ limit: 10 });
    expect(Array.isArray(result)).toBe(true);
  });

  it("agent cannot view audit logs", async () => {
    const caller = appRouter.createCaller(createContext("agent"));
    await expect(caller.auditLogs.list({ limit: 10 })).rejects.toThrow();
  });

  it("audit log is created on role change", async () => {
    const caller = appRouter.createCaller(createContext("admin"));
    await caller.users.updateRole({ userId: 2, role: "admin" });
    expect(mockAuditLogs.length).toBeGreaterThan(0);
    expect(mockAuditLogs[0].action).toBe("role_change");
  });

  it("audit log is created on user deletion", async () => {
    const caller = appRouter.createCaller(createContext("admin"));
    await caller.users.delete({ userId: 2 });
    expect(mockAuditLogs.some(l => l.action === "user_deleted")).toBe(true);
  });

  it("audit log is created on invitation send", async () => {
    mockInvitations.length = 0;
    inviteIdCounter = 1;
    const caller = appRouter.createCaller(createContext("admin"));
    await caller.invitations.send({ email: "auditlog@example.com", role: "agent" });
    expect(mockAuditLogs.some(l => l.action === "invitation_sent")).toBe(true);
  });
});

/* ═══════════════════════════════════════════════════════════════
   CSV EXPORT
   ═══════════════════════════════════════════════════════════════ */

describe("CSV export", () => {
  it("admin can export users as CSV", async () => {
    const caller = appRouter.createCaller(createContext("admin"));
    const result = await caller.users.exportCsv();
    expect(result).toHaveProperty("csv");
    expect(result).toHaveProperty("count", 3);
    expect(result.csv).toContain("ID,Name,Email,Role");
    expect(result.csv).toContain("admin@homelegance.com");
  });

  it("agent cannot export CSV", async () => {
    const caller = appRouter.createCaller(createContext("agent"));
    await expect(caller.users.exportCsv()).rejects.toThrow();
  });
});
