// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { IFileStorage, SystemConfig } from '@citrineos/base';
import type { ILogObj } from 'tslog';
import { Logger } from 'tslog';
import { faker } from '@faker-js/faker';
import { Acme } from '../../../certificate/client/acme.js';
import { aValidSignedCertificate } from '../../providers/ACME.js';
import * as CertificateUtil from '../../../certificate/CertificateUtil.js';
import { Client } from 'acme-client';
import { beforeAll, beforeEach, describe, expect, it, Mock, Mocked, vi } from 'vitest';

vi.mock('../../../certificate/CertificateUtil');

describe('ACME', () => {
  const mockTlsCertificateChain = faker.lorem.word();
  const mockMtlsCertificateAuthorityKey = faker.lorem.word();
  let mockCertUtil: Mocked<typeof CertificateUtil>;
  let mockClient: Mocked<Client>;
  let mockFileStorage: IFileStorage;

  let systemConfig: SystemConfig;
  const logger: Logger<ILogObj> | undefined = undefined;
  let acme: Acme;

  beforeAll(async () => {
    global.fetch = vi.fn();
    mockCertUtil = CertificateUtil as Mocked<typeof CertificateUtil>;

    mockFileStorage = {
      saveFile: vi.fn().mockResolvedValue(undefined),
      getFile: vi
        .fn()
        .mockResolvedValueOnce(mockTlsCertificateChain)
        .mockResolvedValueOnce(mockMtlsCertificateAuthorityKey)
        .mockResolvedValueOnce(faker.lorem.word()),
      exists: vi.fn().mockResolvedValue(true),
      createDirectory: vi.fn().mockResolvedValue(undefined),
      deleteFile: vi.fn().mockResolvedValue(undefined),
    } as unknown as IFileStorage;

    systemConfig = {
      util: {
        networkConnection: {
          websocketServers: [
            {
              id: '3',
              securityProfile: 3,
              tlsCertificateChainFilePath: faker.lorem.word(),
              mtlsCertificateAuthorityKeyFilePath: faker.lorem.word(),
            },
          ],
        },
        certificateAuthority: {
          chargingStationCA: {
            name: 'acme',
            acme: {
              env: 'staging',
              accountKeyFilePath: faker.lorem.word(),
            },
          },
        },
      },
    } as any;
    mockClient = {} as unknown as Mocked<Client>;
    acme = await Acme.create(systemConfig, mockFileStorage, logger, mockClient);
  });

  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('getCertificateChain', () => {
    it('succeeds', async () => {
      const mockLeafPem = faker.lorem.word();
      const mockSubCAPem = faker.lorem.word();
      const mockCertificate = aValidSignedCertificate();
      mockCertUtil.parseCertificateChainPem.mockReturnValueOnce([mockLeafPem, mockSubCAPem]);
      mockCertUtil.createSignedCertificateFromCSR.mockReturnValueOnce(mockCertificate);

      const givenCSR = faker.lorem.word();
      const actualResult = await acme.getCertificateChain(givenCSR);

      const expectedResult = mockCertificate.getPEM().replace(/\n+$/, '') + '\n' + mockSubCAPem;
      expect(actualResult).toBe(expectedResult);
      expect(mockCertUtil.parseCertificateChainPem).toHaveBeenCalledWith(mockTlsCertificateChain);
      expect(mockCertUtil.createSignedCertificateFromCSR).toHaveBeenCalledWith(
        givenCSR,
        mockSubCAPem,
        mockMtlsCertificateAuthorityKey,
      );
    });
  });

  describe('signCertificateByExternalCA', () => {
    const folderPath = '/usr/local/apps/citrineos/Server/src/assets/.well-known/acme-challenge';

    it('creates directory, saves challenge file, and removes on cleanup when directory does not exist', async () => {
      const mockCert = faker.lorem.word();
      const mockToken = faker.lorem.word();
      const mockKeyAuth = faker.lorem.word();
      const mockAuthz = { identifier: { value: faker.internet.domainName() } };

      (mockFileStorage.exists as Mock).mockResolvedValueOnce(false);
      (mockClient as any).auto = vi.fn().mockImplementation(async (options: any) => {
        await options.challengeCreateFn(mockAuthz, { token: mockToken }, mockKeyAuth);
        await options.challengeRemoveFn({}, {}, '');
        return mockCert;
      });

      const givenCSR = faker.lorem.word();
      const actualResult = await acme.signCertificateByExternalCA(givenCSR);

      expect(actualResult).toBe(mockCert);
      expect(mockFileStorage.exists).toHaveBeenCalledWith(folderPath);
      expect(mockFileStorage.createDirectory).toHaveBeenCalledWith(folderPath, { recursive: true });
      expect(mockFileStorage.saveFile).toHaveBeenCalledWith(
        `${folderPath}/${mockToken}`,
        Buffer.from(mockKeyAuth),
      );
      expect(mockFileStorage.deleteFile).toHaveBeenCalledWith(folderPath, {
        recursive: true,
        force: true,
      });
    });

    it('skips directory creation when directory already exists', async () => {
      const mockCert = faker.lorem.word();
      const mockToken = faker.lorem.word();
      const mockKeyAuth = faker.lorem.word();

      (mockFileStorage.exists as Mock).mockResolvedValueOnce(true);
      (mockClient as any).auto = vi.fn().mockImplementation(async (options: any) => {
        await options.challengeCreateFn(
          { identifier: { value: faker.internet.domainName() } },
          { token: mockToken },
          mockKeyAuth,
        );
        await options.challengeRemoveFn({}, {}, '');
        return mockCert;
      });

      const givenCSR = faker.lorem.word();
      const actualResult = await acme.signCertificateByExternalCA(givenCSR);

      expect(actualResult).toBe(mockCert);
      expect(mockFileStorage.exists).toHaveBeenCalledWith(folderPath);
      expect(mockFileStorage.createDirectory).not.toHaveBeenCalled();
      expect(mockFileStorage.deleteFile).toHaveBeenCalledWith(folderPath, {
        recursive: true,
        force: true,
      });
    });

    it('throws when client returns no certificate', async () => {
      (mockClient as any).auto = vi.fn().mockResolvedValue(undefined);

      await expect(acme.signCertificateByExternalCA(faker.lorem.word())).rejects.toThrow(
        'Failed to get signed certificate',
      );
    });
  });

  describe('getRootCACertificate', () => {
    it('success', async () => {
      const mockResult = faker.lorem.word();
      (fetch as Mock).mockReturnValueOnce(
        Promise.resolve({
          ok: true,
          text: () => mockResult,
        }),
      );

      const actualResult = await acme.getRootCACertificate();
      expect(actualResult).toBe(mockResult);

      const expectedUrl = 'https://letsencrypt.org/certs/isrgrootx1.pem';
      expect(fetch).toHaveBeenCalledWith(expectedUrl);
    });

    it('fails due to internal server error', async () => {
      (fetch as Mock).mockReturnValueOnce(
        Promise.resolve({
          status: 500,
          text: () => Promise.resolve('Internal Server Error'),
        }),
      );

      await expect(() => acme.getRootCACertificate()).rejects.toThrow(
        'Failed to fetch certificate: 500: Internal Server Error',
      );
    });
  });
});
