import { assertEquals, assertRejects } from 'jsr:@std/assert';
import { describe, it } from 'jsr:@std/testing/bdd';
import { TypedRequest } from '@api.global/typedrequest';
import {
  createTestContainer,
  getTestPorts,
  loginAndGetIdentity,
  TEST_ACCESS_KEY,
} from './helpers/server.helper.ts';
import { ObjectStorageContainer } from '../ts/index.ts';
import type { IReq_CreateBucket, IReq_ListBuckets } from '../ts_interfaces/requests/buckets.ts';
import type {
  IReq_AddCredential,
  IReq_GetCredentials,
  IReq_RemoveCredential,
} from '../ts_interfaces/requests/credentials.ts';
import type * as interfaces from '../ts_interfaces/index.ts';

const PORT_INDEX = 8;
const ports = getTestPorts(PORT_INDEX);
const url = `http://localhost:${ports.uiPort}/typedrequest`;
const storageDirectory = `.nogit/testdata-${PORT_INDEX}`;

const cleanupStorageDirectory = async () => {
  try {
    await Deno.remove(storageDirectory, { recursive: true });
  } catch (error) {
    if (!(error instanceof Deno.errors.NotFound)) {
      throw error;
    }
  }
};

describe('Credential persistence', { sanitizeResources: false, sanitizeOps: false }, () => {
  it('persists managed credentials across restart and refreshes the internal client', async () => {
    await cleanupStorageDirectory();

    let activeContainer: ObjectStorageContainer | null = null;

    const stopContainer = async () => {
      if (!activeContainer) {
        return;
      }

      try {
        await activeContainer.stop();
      } finally {
        activeContainer = null;
      }
    };

    try {
      activeContainer = createTestContainer(PORT_INDEX);
      await activeContainer.start();

      let identity: interfaces.data.IIdentity = await loginAndGetIdentity(ports.uiPort);

      const addCredential = new TypedRequest<IReq_AddCredential>(url, 'addCredential');
      await addCredential.fire({
        identity,
        accessKeyId: 'persisted-key',
        secretAccessKey: 'persisted-secret',
      });

      const removeCredential = new TypedRequest<IReq_RemoveCredential>(url, 'removeCredential');
      await removeCredential.fire({ identity, accessKeyId: TEST_ACCESS_KEY });

      const getCredentials = new TypedRequest<IReq_GetCredentials>(url, 'getCredentials');
      const credentialsBeforeRestart = await getCredentials.fire({ identity });
      assertEquals(credentialsBeforeRestart.credentials.length, 1);
      assertEquals(credentialsBeforeRestart.credentials[0].accessKeyId, 'persisted-key');

      const listBuckets = new TypedRequest<IReq_ListBuckets>(url, 'listBuckets');
      const bucketsBeforeRestart = await listBuckets.fire({ identity });
      assertEquals(Array.isArray(bucketsBeforeRestart.buckets), true);

      await stopContainer();

      activeContainer = createTestContainer(PORT_INDEX);
      await activeContainer.start();

      identity = await loginAndGetIdentity(ports.uiPort);

      const credentialsAfterRestart = await getCredentials.fire({ identity });
      assertEquals(credentialsAfterRestart.credentials.length, 1);
      assertEquals(credentialsAfterRestart.credentials[0].accessKeyId, 'persisted-key');

      const createBucket = new TypedRequest<IReq_CreateBucket>(url, 'createBucket');
      await createBucket.fire({ identity, bucketName: 'persisted-creds-bucket' });

      const bucketsAfterRestart = await listBuckets.fire({ identity });
      assertEquals(
        bucketsAfterRestart.buckets.some((bucket) => bucket.name === 'persisted-creds-bucket'),
        true,
      );
    } finally {
      await stopContainer();
      await cleanupStorageDirectory();
    }
  });

  it('lets explicit environment credentials override persisted managed credentials', async () => {
    const portIndex = 10;
    const envPorts = getTestPorts(portIndex);
    const envUrl = `http://localhost:${envPorts.uiPort}/typedrequest`;
    const envStorageDirectory = `.nogit/testdata-${portIndex}`;
    const previousAccessKey = Deno.env.get('OBJST_ACCESS_KEY');
    const previousSecretKey = Deno.env.get('OBJST_SECRET_KEY');
    let container: ObjectStorageContainer | null = null;

    const cleanupEnvStorageDirectory = async () => {
      try {
        await Deno.remove(envStorageDirectory, { recursive: true });
      } catch (error) {
        if (!(error instanceof Deno.errors.NotFound)) {
          throw error;
        }
      }
    };

    try {
      await cleanupEnvStorageDirectory();
      await Deno.mkdir(`${envStorageDirectory}/.objectstorage`, { recursive: true });
      await Deno.writeTextFile(
        `${envStorageDirectory}/.objectstorage/admin-config.json`,
        JSON.stringify({
          accessCredentials: [{ accessKeyId: 'persisted-key', secretAccessKey: 'persisted-secret' }],
        }),
      );

      Deno.env.set('OBJST_ACCESS_KEY', 'env-key');
      Deno.env.set('OBJST_SECRET_KEY', 'env-secret');

      container = createTestContainer(portIndex, { storageDirectory: envStorageDirectory });
      await container.start();

      const identity = await loginAndGetIdentity(envPorts.uiPort);
      const getCredentials = new TypedRequest<IReq_GetCredentials>(envUrl, 'getCredentials');
      const response = await getCredentials.fire({ identity });

      assertEquals(response.credentials.length, 1);
      assertEquals(response.credentials[0].accessKeyId, 'env-key');
    } finally {
      if (container) {
        await container.stop();
      }
      if (previousAccessKey === undefined) {
        Deno.env.delete('OBJST_ACCESS_KEY');
      } else {
        Deno.env.set('OBJST_ACCESS_KEY', previousAccessKey);
      }
      if (previousSecretKey === undefined) {
        Deno.env.delete('OBJST_SECRET_KEY');
      } else {
        Deno.env.set('OBJST_SECRET_KEY', previousSecretKey);
      }
      await cleanupEnvStorageDirectory();
    }
  });

  it('does not persist rejected credential replacements', async () => {
    const portIndex = 11;
    const rejectPorts = getTestPorts(portIndex);
    const rejectUrl = `http://localhost:${rejectPorts.uiPort}/typedrequest`;
    const rejectStorageDirectory = `.nogit/testdata-${portIndex}`;
    let container: ObjectStorageContainer | null = null;

    const cleanupRejectStorageDirectory = async () => {
      try {
        await Deno.remove(rejectStorageDirectory, { recursive: true });
      } catch (error) {
        if (!(error instanceof Deno.errors.NotFound)) {
          throw error;
        }
      }
    };

    try {
      await cleanupRejectStorageDirectory();
      container = createTestContainer(portIndex, { storageDirectory: rejectStorageDirectory });
      await container.start();

      await assertRejects(() =>
        container!.replaceAccessCredentials([
          { accessKeyId: 'duplicate-key', secretAccessKey: 'secret-a' },
          { accessKeyId: 'duplicate-key', secretAccessKey: 'secret-b' },
        ])
      );

      const identity = await loginAndGetIdentity(rejectPorts.uiPort);
      const getCredentials = new TypedRequest<IReq_GetCredentials>(rejectUrl, 'getCredentials');
      const response = await getCredentials.fire({ identity });

      assertEquals(response.credentials.length, 1);
      assertEquals(response.credentials[0].accessKeyId, TEST_ACCESS_KEY);
      await assertRejects(() =>
        Deno.readTextFile(`${rejectStorageDirectory}/.objectstorage/admin-config.json`)
      );
    } finally {
      if (container) {
        await container.stop();
      }
      await cleanupRejectStorageDirectory();
    }
  });
});
