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

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

/* ─── Context helpers ─── */
function createPublicContext() {
  const cookies: { name: string; value: string; options: Record<string, unknown> }[] = [];
  const clearedCookies: { name: string; options: Record<string, unknown> }[] = [];

  const ctx: TrpcContext = {
    user: null,
    req: { protocol: "https", headers: {} } as TrpcContext["req"],
    res: {
      cookie: (name: string, value: string, options: Record<string, unknown>) => {
        cookies.push({ name, value, options });
      },
      clearCookie: (name: string, options: Record<string, unknown>) => {
        clearedCookies.push({ name, options });
      },
    } as unknown as TrpcContext["res"],
  };

  return { ctx, cookies, clearedCookies };
}

function createAuthContext(role: "admin" | "agent" | "user") {
  const user: AuthenticatedUser = {
    id: 1,
    openId: "test-openid",
    email: "test@homelegance.com",
    name: "Test User",
    loginMethod: "password",
    role,
    createdAt: new Date(),
    updatedAt: new Date(),
    lastSignedIn: new Date(),
  };

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

/* ─── Mock database ─── */
const mockUsers: any[] = [];
const mockResetTokens: any[] = [];
let userIdCounter = 1;
let tokenIdCounter = 1;

vi.mock("./db", () => ({
  getUserByEmailWithPassword: vi.fn(async (email: string) => {
    return mockUsers.find(u => u.email === email) || null;
  }),
  createUserWithPassword: vi.fn(async (data: any) => {
    const user = {
      id: userIdCounter++,
      openId: `email-${data.email}`,
      email: data.email,
      name: data.name,
      passwordHash: data.passwordHash,
      role: "user",
      loginMethod: "password",
      createdAt: new Date(),
      updatedAt: new Date(),
      lastSignedIn: new Date(),
    };
    mockUsers.push(user);
    return user;
  }),
  updateUserPassword: vi.fn(async (userId: number, hash: string) => {
    const user = mockUsers.find(u => u.id === userId);
    if (user) user.passwordHash = hash;
  }),
  createPasswordResetToken: vi.fn(async (data: any) => {
    const token = { id: tokenIdCounter++, ...data, usedAt: null, createdAt: new Date() };
    mockResetTokens.push(token);
    return token;
  }),
  getPasswordResetToken: vi.fn(async (token: string) => {
    return mockResetTokens.find(t => t.token === token) || null;
  }),
  markPasswordResetTokenUsed: vi.fn(async (id: number) => {
    const token = mockResetTokens.find(t => t.id === id);
    if (token) token.usedAt = new Date();
  }),
  getUserById: vi.fn(async (id: number) => {
    return mockUsers.find(u => u.id === id) || null;
  }),
  // Other db exports needed by routers
  createConversation: vi.fn(async (data: any) => ({ id: 1, ...data })),
  getConversations: vi.fn(async () => []),
  getConversationById: vi.fn(async () => null),
  getConversationBySessionId: vi.fn(async () => null),
  updateConversationStatus: vi.fn(async () => null),
  getConversationStats: vi.fn(async () => ({ total: 0, active: 0, escalated: 0, resolved: 0, closed: 0 })),
  addMessage: vi.fn(async (data: any) => ({ id: 1, ...data })),
  getMessagesByConversation: vi.fn(async () => []),
  saveWorkflow: vi.fn(async () => ({})),
  getWorkflow: vi.fn(async () => ({ nodes: [], edges: [] })),
  getAllUsers: vi.fn(async () => []),
  updateUserRole: vi.fn(async () => null),
  getUserByEmail: vi.fn(async () => null),
  deleteUser: vi.fn(async () => null),
  createInvitation: vi.fn(async () => ({})),
  getAllInvitations: vi.fn(async () => []),
  getInvitationByToken: vi.fn(async () => null),
  getInvitationByEmail: vi.fn(async () => []),
  updateInvitationStatus: vi.fn(async () => null),
  expireOldInvitations: vi.fn(async () => {}),
  createAuditLog: vi.fn(async () => ({})),
  getAuditLogs: vi.fn(async () => []),
  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" }, finish_reason: "stop" }],
  })),
}));

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

vi.mock("./_core/sdk", () => ({
  sdk: {
    createSessionToken: vi.fn(async () => "mock-session-token"),
  },
}));

vi.mock("bcryptjs", () => ({
  default: {
    hash: vi.fn(async (password: string) => `hashed-${password}`),
    compare: vi.fn(async (password: string, hash: string) => hash === `hashed-${password}`),
  },
}));

beforeEach(() => {
  mockUsers.length = 0;
  mockResetTokens.length = 0;
  userIdCounter = 1;
  tokenIdCounter = 1;
});

/* ═══════════════════════════════════════════════════════════════
   REGISTRATION
   ═══════════════════════════════════════════════════════════════ */

describe("auth.register", () => {
  it("creates a new user and sets session cookie", async () => {
    const { ctx, cookies } = createPublicContext();
    const caller = appRouter.createCaller(ctx);

    const result = await caller.auth.register({
      email: "new@homelegance.com",
      password: "StrongPass1",
      name: "New User",
    });

    expect(result.success).toBe(true);
    expect(result.user.email).toBe("new@homelegance.com");
    expect(result.user.name).toBe("New User");
    expect(result.user.role).toBe("user");
    expect(cookies.length).toBe(1);
    expect(cookies[0].name).toBe(COOKIE_NAME);
    expect(cookies[0].value).toBe("mock-session-token");
  });

  it("rejects duplicate email registration", async () => {
    const { ctx: ctx1 } = createPublicContext();
    const caller1 = appRouter.createCaller(ctx1);
    await caller1.auth.register({
      email: "dup@homelegance.com",
      password: "StrongPass1",
      name: "First User",
    });

    const { ctx: ctx2 } = createPublicContext();
    const caller2 = appRouter.createCaller(ctx2);
    await expect(
      caller2.auth.register({
        email: "dup@homelegance.com",
        password: "AnotherPass1",
        name: "Second User",
      })
    ).rejects.toThrow("An account with this email already exists");
  });

  it("rejects password shorter than 8 characters", async () => {
    const { ctx } = createPublicContext();
    const caller = appRouter.createCaller(ctx);
    await expect(
      caller.auth.register({
        email: "short@homelegance.com",
        password: "Short1",
        name: "Short Pass",
      })
    ).rejects.toThrow();
  });
});

/* ═══════════════════════════════════════════════════════════════
   LOGIN
   ═══════════════════════════════════════════════════════════════ */

describe("auth.login", () => {
  it("logs in with correct credentials and sets session cookie", async () => {
    // First register a user
    const { ctx: regCtx } = createPublicContext();
    await appRouter.createCaller(regCtx).auth.register({
      email: "login@homelegance.com",
      password: "CorrectPass1",
      name: "Login User",
    });

    // Now login
    const { ctx, cookies } = createPublicContext();
    const caller = appRouter.createCaller(ctx);
    const result = await caller.auth.login({
      email: "login@homelegance.com",
      password: "CorrectPass1",
    });

    expect(result.success).toBe(true);
    expect(result.user.email).toBe("login@homelegance.com");
    expect(cookies.length).toBe(1);
    expect(cookies[0].name).toBe(COOKIE_NAME);
  });

  it("rejects login with wrong password", async () => {
    const { ctx: regCtx } = createPublicContext();
    await appRouter.createCaller(regCtx).auth.register({
      email: "wrong@homelegance.com",
      password: "CorrectPass1",
      name: "Wrong Pass User",
    });

    const { ctx } = createPublicContext();
    const caller = appRouter.createCaller(ctx);
    await expect(
      caller.auth.login({
        email: "wrong@homelegance.com",
        password: "WrongPassword1",
      })
    ).rejects.toThrow("Invalid email or password");
  });

  it("rejects login with non-existent email", async () => {
    const { ctx } = createPublicContext();
    const caller = appRouter.createCaller(ctx);
    await expect(
      caller.auth.login({
        email: "nonexistent@homelegance.com",
        password: "SomePass1",
      })
    ).rejects.toThrow("Invalid email or password");
  });
});

/* ═══════════════════════════════════════════════════════════════
   FORGOT PASSWORD
   ═══════════════════════════════════════════════════════════════ */

describe("auth.forgotPassword", () => {
  it("returns success and token for existing user", async () => {
    // Register a user first
    const { ctx: regCtx } = createPublicContext();
    await appRouter.createCaller(regCtx).auth.register({
      email: "forgot@homelegance.com",
      password: "OldPass123",
      name: "Forgot User",
    });

    const { ctx } = createPublicContext();
    const caller = appRouter.createCaller(ctx);
    const result = await caller.auth.forgotPassword({
      email: "forgot@homelegance.com",
    });

    expect(result.success).toBe(true);
    expect(result.resetToken).toBeDefined();
    expect(typeof result.resetToken).toBe("string");
  });

  it("returns success even for non-existent email (prevents enumeration)", async () => {
    const { ctx } = createPublicContext();
    const caller = appRouter.createCaller(ctx);
    const result = await caller.auth.forgotPassword({
      email: "nobody@homelegance.com",
    });

    expect(result.success).toBe(true);
    expect(result.resetToken).toBeUndefined();
  });
});

/* ═══════════════════════════════════════════════════════════════
   VALIDATE RESET TOKEN
   ═══════════════════════════════════════════════════════════════ */

describe("auth.validateResetToken", () => {
  it("validates a valid token", async () => {
    // Register and request reset
    const { ctx: regCtx } = createPublicContext();
    await appRouter.createCaller(regCtx).auth.register({
      email: "validate@homelegance.com",
      password: "OldPass123",
      name: "Validate User",
    });

    const { ctx: forgotCtx } = createPublicContext();
    const forgotResult = await appRouter.createCaller(forgotCtx).auth.forgotPassword({
      email: "validate@homelegance.com",
    });

    const { ctx } = createPublicContext();
    const caller = appRouter.createCaller(ctx);
    const result = await caller.auth.validateResetToken({
      token: forgotResult.resetToken!,
    });

    expect(result.valid).toBe(true);
    if (result.valid) {
      expect(result.email).toBe("validate@homelegance.com");
    }
  });

  it("rejects an invalid token", async () => {
    const { ctx } = createPublicContext();
    const caller = appRouter.createCaller(ctx);
    const result = await caller.auth.validateResetToken({
      token: "invalid-token-xyz",
    });

    expect(result.valid).toBe(false);
  });

  it("rejects an expired token", async () => {
    // Register and create an expired token manually
    const { ctx: regCtx } = createPublicContext();
    await appRouter.createCaller(regCtx).auth.register({
      email: "expired@homelegance.com",
      password: "OldPass123",
      name: "Expired User",
    });

    // Manually add an expired token
    mockResetTokens.push({
      id: tokenIdCounter++,
      userId: 1,
      token: "expired-token",
      expiresAt: new Date(Date.now() - 1000), // expired 1 second ago
      usedAt: null,
      createdAt: new Date(),
    });

    const { ctx } = createPublicContext();
    const caller = appRouter.createCaller(ctx);
    const result = await caller.auth.validateResetToken({
      token: "expired-token",
    });

    expect(result.valid).toBe(false);
    if (!result.valid) {
      expect(result.reason).toContain("expired");
    }
  });

  it("rejects an already-used token", async () => {
    mockResetTokens.push({
      id: tokenIdCounter++,
      userId: 1,
      token: "used-token",
      expiresAt: new Date(Date.now() + 3600000),
      usedAt: new Date(),
      createdAt: new Date(),
    });

    const { ctx } = createPublicContext();
    const caller = appRouter.createCaller(ctx);
    const result = await caller.auth.validateResetToken({
      token: "used-token",
    });

    expect(result.valid).toBe(false);
    if (!result.valid) {
      expect(result.reason).toContain("already been used");
    }
  });
});

/* ═══════════════════════════════════════════════════════════════
   RESET PASSWORD
   ═══════════════════════════════════════════════════════════════ */

describe("auth.resetPassword", () => {
  it("resets password with a valid token", async () => {
    // Register and request reset
    const { ctx: regCtx } = createPublicContext();
    await appRouter.createCaller(regCtx).auth.register({
      email: "reset@homelegance.com",
      password: "OldPass123",
      name: "Reset User",
    });

    const { ctx: forgotCtx } = createPublicContext();
    const forgotResult = await appRouter.createCaller(forgotCtx).auth.forgotPassword({
      email: "reset@homelegance.com",
    });

    const { ctx } = createPublicContext();
    const caller = appRouter.createCaller(ctx);
    const result = await caller.auth.resetPassword({
      token: forgotResult.resetToken!,
      newPassword: "NewStrongPass1",
    });

    expect(result.success).toBe(true);
  });

  it("rejects reset with invalid token", async () => {
    const { ctx } = createPublicContext();
    const caller = appRouter.createCaller(ctx);
    await expect(
      caller.auth.resetPassword({
        token: "invalid-token",
        newPassword: "NewPass123",
      })
    ).rejects.toThrow("Invalid reset link");
  });

  it("rejects reset with expired token", async () => {
    mockResetTokens.push({
      id: tokenIdCounter++,
      userId: 1,
      token: "expired-reset",
      expiresAt: new Date(Date.now() - 1000),
      usedAt: null,
      createdAt: new Date(),
    });

    const { ctx } = createPublicContext();
    const caller = appRouter.createCaller(ctx);
    await expect(
      caller.auth.resetPassword({
        token: "expired-reset",
        newPassword: "NewPass123",
      })
    ).rejects.toThrow("expired");
  });

  it("rejects reset with already-used token", async () => {
    mockResetTokens.push({
      id: tokenIdCounter++,
      userId: 1,
      token: "used-reset",
      expiresAt: new Date(Date.now() + 3600000),
      usedAt: new Date(),
      createdAt: new Date(),
    });

    const { ctx } = createPublicContext();
    const caller = appRouter.createCaller(ctx);
    await expect(
      caller.auth.resetPassword({
        token: "used-reset",
        newPassword: "NewPass123",
      })
    ).rejects.toThrow("already been used");
  });

  it("rejects new password shorter than 8 characters", async () => {
    // Register and request reset
    const { ctx: regCtx } = createPublicContext();
    await appRouter.createCaller(regCtx).auth.register({
      email: "shortpw@homelegance.com",
      password: "OldPass123",
      name: "Short PW User",
    });

    const { ctx: forgotCtx } = createPublicContext();
    const forgotResult = await appRouter.createCaller(forgotCtx).auth.forgotPassword({
      email: "shortpw@homelegance.com",
    });

    const { ctx } = createPublicContext();
    const caller = appRouter.createCaller(ctx);
    await expect(
      caller.auth.resetPassword({
        token: forgotResult.resetToken!,
        newPassword: "Short1",
      })
    ).rejects.toThrow();
  });
});

/* ═══════════════════════════════════════════════════════════════
   LOGOUT
   ═══════════════════════════════════════════════════════════════ */

describe("auth.logout", () => {
  it("clears the session cookie", async () => {
    const ctx = createAuthContext("admin");
    const caller = appRouter.createCaller(ctx);
    const result = await caller.auth.logout();

    expect(result.success).toBe(true);
    expect(ctx.res.clearCookie).toHaveBeenCalledWith(
      COOKIE_NAME,
      expect.objectContaining({ maxAge: -1 })
    );
  });
});
