import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
import { verifyAccessToken, hashToken } from "../services/auth";
import jwt from "jsonwebtoken";
import { config } from "../config/env";
import prisma from "../config/database";

// Prefix for fixtures so cleanup never touches real data.
const N = "auth_svc_test_";

let testUserId: number;
let testUsername: string;
let testRoleName: string | null;

beforeAll(async () => {
  // Clean up any leftover fixtures from a previous failed run.
  await prisma.users
    .deleteMany({ where: { username: { startsWith: N } } })
    .catch(() => {});

  // Use a real (active, unlocked) user from the seeded DB so loadAuthData
  // returns a full AuthData. We don't need a specific role — any active user
  // works — but we record its role so we can assert roleName precisely.
  const role = await prisma.roles.findFirst();
  if (!role) throw new Error("Test setup: no roles found — seed the database");

  const user = await prisma.users.create({
    data: {
      username: `${N}user`,
      email: `${N}user@test.local`,
      // DUMMY_HASH used elsewhere in the suite; we never verify a password
      // here, only that the token's `sub` resolves to this user.
      password_hash:
        "$2a$12$LJ3m4ys3Lg4oLBFnYP2amuPBzJnJBbGzCl5Y6X9Y8r0q5.s3L6OyO",
      first_name: "Auth",
      last_name: "Service",
      is_active: true,
      role_id: role.id,
    },
    include: { roles: true },
  });
  testUserId = user.id;
  testUsername = user.username;
  testRoleName = user.roles?.name ?? null;
});

afterAll(async () => {
  await prisma.users
    .deleteMany({ where: { username: { startsWith: N } } })
    .catch(() => {});
});

describe("auth service", () => {
  describe("verifyAccessToken", () => {
    it("returns AuthData for a valid token signed for a real user", async () => {
      const token = jwt.sign(
        { sub: testUserId, username: testUsername, role: testRoleName },
        config.jwt.secret,
        { expiresIn: config.jwt.accessTokenExpiry, algorithm: "HS256" },
      );

      const result = await verifyAccessToken(token);
      expect(result).not.toBeNull();
      expect(result!.userId).toBe(testUserId);
      expect(result!.username).toBe(testUsername);
      expect(result!.roleName).toBe(testRoleName);
      // Permissions resolve to an array (admins get all, others get role perms).
      expect(Array.isArray(result!.permissions)).toBe(true);
    });

    it("returns null WITHOUT logging for an invalid JWT (expected condition)", async () => {
      const consoleSpy = vi
        .spyOn(console, "error")
        .mockImplementation(() => {});
      const result = await verifyAccessToken("invalid-token");
      expect(result).toBeNull();
      // Invalid/expired access tokens are an expected condition (the client
      // refreshes on the resulting 401), so verifyAccessToken deliberately
      // does NOT log them as errors — only genuinely unexpected failures.
      expect(consoleSpy).not.toHaveBeenCalled();
      consoleSpy.mockRestore();
    });

    it("returns null WITHOUT logging for an expired JWT", async () => {
      const consoleSpy = vi
        .spyOn(console, "error")
        .mockImplementation(() => {});
      const expiredToken = jwt.sign(
        { sub: 1, username: "test", role: "user" },
        config.jwt.secret,
        { expiresIn: -1 },
      );
      const result = await verifyAccessToken(expiredToken);
      expect(result).toBeNull();
      expect(consoleSpy).not.toHaveBeenCalled();
      consoleSpy.mockRestore();
    });
  });

  describe("hashToken", () => {
    it("matches the known SHA-256 vector for a fixed input", () => {
      // SHA-256("hello") — pins the algorithm so a silent change (e.g. to a
      // different but still 64-hex-deterministic hash) is caught.
      expect(hashToken("hello")).toBe(
        "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
      );
    });

    it("produces deterministic SHA-256 hex output", () => {
      const t1 = hashToken("hello");
      const t2 = hashToken("hello");
      expect(t1).toBe(t2);
      expect(t1).toMatch(/^[a-f0-9]{64}$/);
    });

    it("produces different hashes for different inputs", () => {
      const t1 = hashToken("a");
      const t2 = hashToken("b");
      expect(t1).not.toBe(t2);
    });
  });
});
