import { beforeEach, describe, expect, it, vi } from "vitest";

vi.mock("@/lib/prisma", () => ({
  prisma: {
    auth_users: {
      findUnique: vi.fn(),
    },
  },
}));

vi.mock("bcrypt", () => ({
  default: {
    compare: vi.fn(),
  },
}));

vi.mock("jsonwebtoken", () => ({
  default: {
    sign: vi.fn(),
    verify: vi.fn(),
  },
}));

import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { prisma } from "@/lib/prisma";

import {
  authenticateAuthUser,
  extractBearerToken,
  getTokenExpirationSeconds,
  issueJwtToken,
  verifyAdminCredentials,
  verifyJwtToken,
} from "@/lib/auth";

describe("Unit — auth", () => {
  beforeEach(() => {
    vi.clearAllMocks();

    delete process.env.SECRET_KEY;
    delete process.env.JWT_ALGORITHM;
    delete process.env.JWT_EXPIRATION_HOURS;
    delete process.env.AUTH_ADMIN_USERNAME;
    delete process.env.AUTH_ADMIN_PASSWORD;
    delete process.env.AUTH_ADMIN_PASSWORD_HASH;
  });

  describe("getTokenExpirationSeconds", () => {
    it("returns default expiration in seconds", () => {
      expect(getTokenExpirationSeconds()).toBe(24 * 60 * 60);
    });

    it("returns custom expiration in seconds", () => {
      process.env.JWT_EXPIRATION_HOURS = "2";

      expect(getTokenExpirationSeconds()).toBe(2 * 60 * 60);
    });

    it("falls back to 24h when env value is invalid", () => {
      process.env.JWT_EXPIRATION_HOURS = "invalid";

      expect(getTokenExpirationSeconds()).toBe(24 * 60 * 60);
    });
  });

  describe("extractBearerToken", () => {
    it("returns null when header is missing", () => {
      expect(extractBearerToken(null)).toBeNull();
    });

    it("returns null when scheme is not Bearer", () => {
      expect(extractBearerToken("Basic abc")).toBeNull();
    });

    it("returns null when token is missing", () => {
      expect(extractBearerToken("Bearer")).toBeNull();
    });

    it("returns token when header is valid", () => {
      expect(extractBearerToken("Bearer my-token")).toBe("my-token");
    });
  });

  describe("authenticateAuthUser", () => {
    it("returns user when database user exists, is active, and password matches", async () => {
      vi.mocked(prisma.auth_users.findUnique).mockResolvedValue({
        username: "admin",
        password_hash: "$2b$hash",
        role: "admin",
        is_active: true,
      } as any);

      vi.mocked(bcrypt.compare).mockResolvedValue(true as never);

      const result = await authenticateAuthUser("admin", "password");

      expect(result).toEqual({
        username: "admin",
        role: "admin",
      });
    });

    it("returns null when database user is inactive", async () => {
      vi.mocked(prisma.auth_users.findUnique).mockResolvedValue({
        username: "admin",
        password_hash: "$2b$hash",
        role: "admin",
        is_active: false,
      } as any);

      const result = await authenticateAuthUser("admin", "password");

      expect(result).toBeNull();
    });

    it("returns null when database password does not match", async () => {
      vi.mocked(prisma.auth_users.findUnique).mockResolvedValue({
        username: "admin",
        password_hash: "$2b$hash",
        role: "admin",
        is_active: true,
      } as any);

      vi.mocked(bcrypt.compare).mockResolvedValue(false as never);

      const result = await authenticateAuthUser("admin", "wrong-password");

      expect(result).toBeNull();
    });

    it("falls back to env auth when database throws", async () => {
      vi.mocked(prisma.auth_users.findUnique).mockRejectedValue(new Error("db down"));

      process.env.AUTH_ADMIN_USERNAME = "admin";
      process.env.AUTH_ADMIN_PASSWORD = "password";

      const result = await authenticateAuthUser("admin", "password");

      expect(result).toEqual({
        username: "admin",
        role: "admin",
      });
    });

    it("returns null when env username does not match", async () => {
      vi.mocked(prisma.auth_users.findUnique).mockRejectedValue(new Error("db down"));

      process.env.AUTH_ADMIN_USERNAME = "admin";
      process.env.AUTH_ADMIN_PASSWORD = "password";

      const result = await authenticateAuthUser("other", "password");

      expect(result).toBeNull();
    });

    it("throws when env username exists but no env password is configured", async () => {
      vi.mocked(prisma.auth_users.findUnique).mockRejectedValue(new Error("db down"));

      process.env.AUTH_ADMIN_USERNAME = "admin";

      await expect(authenticateAuthUser("admin", "password")).rejects.toThrow(
        "Missing AUTH_ADMIN_PASSWORD_HASH or AUTH_ADMIN_PASSWORD environment variable",
      );
    });
    it('returns null when env bcrypt password does not match', async () => {
  vi.mocked(prisma.auth_users.findUnique).mockRejectedValue(new Error('db down'));

  process.env.AUTH_ADMIN_USERNAME = 'admin';
  process.env.AUTH_ADMIN_PASSWORD_HASH = '$2b$hash';

  vi.mocked(bcrypt.compare).mockResolvedValue(false as never);

  const result = await authenticateAuthUser('admin', 'wrong-password');

  expect(result).toBeNull();
});
it('returns admin when env bcrypt password matches', async () => {
  vi.mocked(prisma.auth_users.findUnique).mockRejectedValue(new Error('db down'));

  process.env.AUTH_ADMIN_USERNAME = 'admin';
  process.env.AUTH_ADMIN_PASSWORD_HASH = '$2b$hash';

  vi.mocked(bcrypt.compare).mockResolvedValue(true as never);

  const result = await authenticateAuthUser('admin', 'password');

  expect(result).toEqual({
    username: 'admin',
    role: 'admin',
  });
});
it('returns null when env plain password does not match', async () => {
  vi.mocked(prisma.auth_users.findUnique).mockRejectedValue(new Error('db down'));

  process.env.AUTH_ADMIN_USERNAME = 'admin';
  process.env.AUTH_ADMIN_PASSWORD = 'secret';

  const result = await authenticateAuthUser('admin', 'wrong-password');

  expect(result).toBeNull();
});
it('returns admin when env plain password matches', async () => {
  vi.mocked(prisma.auth_users.findUnique).mockRejectedValue(new Error('db down'));

  process.env.AUTH_ADMIN_USERNAME = 'admin';
  process.env.AUTH_ADMIN_PASSWORD = 'secret';

  const result = await authenticateAuthUser('admin', 'secret');

  expect(result).toEqual({
    username: 'admin',
    role: 'admin',
  });
});
  });

  describe("verifyAdminCredentials", () => {
    it("returns true when authentication succeeds", async () => {
      vi.mocked(prisma.auth_users.findUnique).mockResolvedValue({
        username: "admin",
        password_hash: "password",
        role: "admin",
        is_active: true,
      } as any);

      const result = await verifyAdminCredentials("admin", "password");

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

  describe("issueJwtToken", () => {
    it("signs a jwt token", () => {
      process.env.SECRET_KEY = "secret";
      vi.mocked(jwt.sign).mockReturnValue("signed-token" as never);

      const token = issueJwtToken("admin");

      expect(token).toBe("signed-token");
      expect(jwt.sign).toHaveBeenCalledWith(
        {
          sub: "admin",
          role: "admin",
        },
        "secret",
        {
          algorithm: "HS256",
          expiresIn: "24h",
        },
      );
    });

    it("throws when SECRET_KEY is missing", () => {
      expect(() => issueJwtToken("admin")).toThrow(
        "Missing required environment variable: SECRET_KEY",
      );
    });
  });

  describe("verifyJwtToken", () => {
    it("verifies a jwt token", () => {
      process.env.SECRET_KEY = "secret";

      vi.mocked(jwt.verify).mockReturnValue({
        sub: "admin",
        role: "admin",
      } as never);

      const result = verifyJwtToken("token");

      expect(result).toEqual({
        sub: "admin",
        role: "admin",
      });

      expect(jwt.verify).toHaveBeenCalledWith("token", "secret", {
        algorithms: ["HS256"],
      });
    });
  });
});