import { NotFoundException } from "@nestjs/common";
import { compare } from "bcryptjs";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { FleetSettingsService } from "./fleet-settings.service";
import type { PrismaService } from "../prisma/prisma.service";
import type { CryptoService } from "../crypto/crypto.service";

describe("FleetSettingsService", () => {
  let service: FleetSettingsService;

  const mockPrisma = {
    llmProvider: {
      findMany: vi.fn(),
      findFirst: vi.fn(),
      findUnique: vi.fn(),
      create: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
    },
    userAgentConfig: {
      findUnique: vi.fn(),
      upsert: vi.fn(),
    },
    systemConfig: {
      findMany: vi.fn(),
      upsert: vi.fn(),
      deleteMany: vi.fn(),
    },
    breakglassUser: {
      findUnique: vi.fn(),
      update: vi.fn(),
    },
  };

  const mockCrypto = {
    encrypt: vi.fn((value: string) => `enc:${value}`),
  };

  beforeEach(() => {
    vi.clearAllMocks();
    service = new FleetSettingsService(
      mockPrisma as unknown as PrismaService,
      mockCrypto as unknown as CryptoService
    );
  });

  it("listProviders returns only providers for the given userId", async () => {
    mockPrisma.llmProvider.findMany.mockResolvedValue([
      {
        id: "prov-1",
        name: "openai-main",
        displayName: "OpenAI",
        type: "openai",
        baseUrl: "https://api.openai.com/v1",
        isActive: true,
        models: [{ id: "gpt-4.1" }],
      },
    ]);

    const result = await service.listProviders("user-1");

    expect(mockPrisma.llmProvider.findMany).toHaveBeenCalledWith({
      where: { userId: "user-1" },
      select: {
        id: true,
        name: true,
        displayName: true,
        type: true,
        baseUrl: true,
        isActive: true,
        models: true,
      },
      orderBy: { createdAt: "asc" },
    });
    expect(result).toEqual([
      {
        id: "prov-1",
        name: "openai-main",
        displayName: "OpenAI",
        type: "openai",
        baseUrl: "https://api.openai.com/v1",
        isActive: true,
        models: [{ id: "gpt-4.1" }],
      },
    ]);
  });

  it("createProvider encrypts apiKey", async () => {
    mockPrisma.llmProvider.create.mockResolvedValue({
      id: "prov-2",
    });

    const result = await service.createProvider("user-1", {
      name: "zai-main",
      displayName: "Z.ai",
      type: "zai",
      apiKey: "plaintext-key",
      models: [],
    });

    expect(mockCrypto.encrypt).toHaveBeenCalledWith("plaintext-key");
    expect(mockPrisma.llmProvider.create).toHaveBeenCalledWith({
      data: {
        userId: "user-1",
        name: "zai-main",
        displayName: "Z.ai",
        type: "zai",
        baseUrl: null,
        apiKey: "enc:plaintext-key",
        apiType: "openai-completions",
        models: [],
      },
      select: {
        id: true,
      },
    });
    expect(result).toEqual({ id: "prov-2" });
  });

  it("updateProvider rejects if not owned by user", async () => {
    mockPrisma.llmProvider.findFirst.mockResolvedValue(null);

    await expect(
      service.updateProvider("user-1", "provider-1", {
        displayName: "New Name",
      })
    ).rejects.toBeInstanceOf(NotFoundException);

    expect(mockPrisma.llmProvider.update).not.toHaveBeenCalled();
  });

  it("deleteProvider rejects if not owned by user", async () => {
    mockPrisma.llmProvider.findFirst.mockResolvedValue(null);

    await expect(service.deleteProvider("user-1", "provider-1")).rejects.toBeInstanceOf(
      NotFoundException
    );

    expect(mockPrisma.llmProvider.delete).not.toHaveBeenCalled();
  });

  it("getOidcConfig never returns clientSecret", async () => {
    mockPrisma.systemConfig.findMany.mockResolvedValue([
      {
        key: "oidc.issuerUrl",
        value: "https://issuer.example.com",
      },
      {
        key: "oidc.clientId",
        value: "client-id-1",
      },
      {
        key: "oidc.clientSecret",
        value: "enc:very-secret",
      },
    ]);

    const result = await service.getOidcConfig();

    expect(result).toEqual({
      issuerUrl: "https://issuer.example.com",
      clientId: "client-id-1",
      configured: true,
    });
    expect(result).not.toHaveProperty("clientSecret");
  });

  it("updateOidcConfig encrypts clientSecret", async () => {
    await service.updateOidcConfig({
      issuerUrl: "https://issuer.example.com",
      clientId: "client-id-1",
      clientSecret: "super-secret",
    });

    expect(mockCrypto.encrypt).toHaveBeenCalledWith("super-secret");
    expect(mockPrisma.systemConfig.upsert).toHaveBeenCalledTimes(3);
    expect(mockPrisma.systemConfig.upsert).toHaveBeenCalledWith({
      where: { key: "oidc.clientSecret" },
      update: { value: "enc:super-secret", encrypted: true },
      create: { key: "oidc.clientSecret", value: "enc:super-secret", encrypted: true },
    });
  });

  it("resetBreakglassPassword hashes new password", async () => {
    mockPrisma.breakglassUser.findUnique.mockResolvedValue({
      id: "bg-1",
      username: "admin",
      passwordHash: "old-hash",
    });

    await service.resetBreakglassPassword("admin", "new-password-123");

    expect(mockPrisma.breakglassUser.update).toHaveBeenCalledOnce();
    const updateCall = mockPrisma.breakglassUser.update.mock.calls[0]?.[0];
    const newHash = updateCall?.data?.passwordHash;
    expect(newHash).toBeTypeOf("string");
    expect(newHash).not.toBe("new-password-123");
    expect(await compare("new-password-123", newHash as string)).toBe(true);
  });
});
