/*
 * Copyright (c) 2022 Airbyte, Inc., all rights reserved.
 */

package io.airbyte.config.persistence.split_secrets;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.fasterxml.jackson.databind.JsonNode;
import io.airbyte.config.persistence.split_secrets.test_cases.ArrayOneOfTestCase;
import io.airbyte.config.persistence.split_secrets.test_cases.ArrayTestCase;
import io.airbyte.config.persistence.split_secrets.test_cases.NestedObjectTestCase;
import io.airbyte.config.persistence.split_secrets.test_cases.NestedOneOfTestCase;
import io.airbyte.config.persistence.split_secrets.test_cases.OneOfSecretTestCase;
import io.airbyte.config.persistence.split_secrets.test_cases.OneOfTestCase;
import io.airbyte.config.persistence.split_secrets.test_cases.OptionalPasswordTestCase;
import io.airbyte.config.persistence.split_secrets.test_cases.PostgresSshKeyTestCase;
import io.airbyte.config.persistence.split_secrets.test_cases.SimpleTestCase;
import io.airbyte.validation.json.JsonSchemaValidator;
import io.airbyte.validation.json.JsonValidationException;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

@SuppressWarnings({"PMD.JUnit5TestShouldBePackagePrivate", "PMD.UnusedPrivateMethod"})
public class SecretsHelpersTest {

  public static final UUID WORKSPACE_ID = UUID.fromString("e0eb0554-ffe0-4e9c-9dc0-ed7f52023eb2");

  // use a fixed sequence of UUIDs so it's easier to have static files for the test cases
  public static final List<UUID> UUIDS = List.of(
      UUID.fromString("9eba44d8-51e7-48f1-bde2-619af0e42c22"),
      UUID.fromString("2c2ef2b3-259a-4e73-96d1-f56dacee2e5e"),
      UUID.fromString("1206db5b-b968-4df1-9a76-f3fcdae7e307"),
      UUID.fromString("c03ef566-79a7-4e77-b6f3-d23d2528f25a"),
      UUID.fromString("35f08b15-bfd9-44fe-a8c7-5aa9e156c0f5"),
      UUID.fromString("159c0b6f-f9ae-48b4-b7f3-bcac4ba15743"),
      UUID.fromString("71af9b74-4e61-4cff-830e-3bf1ec18fbc0"),
      UUID.fromString("067a62fc-d007-44dd-a8f6-0fd10823713d"),
      UUID.fromString("c4967ac9-0856-4733-a21e-1d51ca8f254d"));

  private static final String PROVIDE_TEST_CASES = "provideTestCases";

  /**
   * This is a bit of a non-standard way of specifying test case paramaterization for Junit, but it's
   * intended to let you treat most of the JSON involved in the tests as static files.
   */
  private static Stream<Arguments> provideTestCases() {
    return Stream.of(
        new OptionalPasswordTestCase(),
        new SimpleTestCase(),
        new NestedObjectTestCase(),
        new OneOfTestCase(),
        new OneOfSecretTestCase(),
        new ArrayTestCase(),
        new ArrayOneOfTestCase(),
        new NestedOneOfTestCase(),
        new PostgresSshKeyTestCase()).map(Arguments::of);
  }

  @ParameterizedTest
  @MethodSource(PROVIDE_TEST_CASES)
  public void validateTestCases(final SecretsTestCase testCase) throws JsonValidationException {
    final var validator = new JsonSchemaValidator();
    final var spec = testCase.getSpec().getConnectionSpecification();
    validator.ensure(spec, testCase.getFullConfig());
    validator.ensure(spec, testCase.getUpdateConfig());
  }

  @ParameterizedTest
  @MethodSource(PROVIDE_TEST_CASES)
  void testSplit(final SecretsTestCase testCase) {
    final var uuidIterator = UUIDS.iterator();
    final var inputConfig = testCase.getFullConfig();
    final var inputConfigCopy = inputConfig.deepCopy();
    final var splitConfig = SecretsHelpers.splitConfig(
        uuidIterator::next,
        WORKSPACE_ID,
        inputConfig,
        testCase.getSpec());

    assertEquals(testCase.getPartialConfig(), splitConfig.getPartialConfig());
    assertEquals(testCase.getFirstSecretMap(), splitConfig.getCoordinateToPayload());

    // check that we didn't mutate the input configs
    assertEquals(inputConfigCopy, inputConfig);

    // check that keys for Google Secrets Manger fit the requirements:
    // A secret ID is a string with a maximum length of 255 characters and can contain
    // uppercase and lowercase letters, numerals, and the hyphen (-) and underscore (_) characters.
    // https://cloud.google.com/secret-manager/docs/reference/rpc/google.cloud.secretmanager.v1#createsecretrequest
    final var gsmKeyCharacterPattern = Pattern.compile("^[a-zA-Z0-9_-]+$");

    // sanity check pattern with a character that isn't allowed
    assertFalse(gsmKeyCharacterPattern.matcher("/").matches());

    // check every key for the pattern and max length
    splitConfig.getCoordinateToPayload().keySet().forEach(key -> {
      assertTrue(gsmKeyCharacterPattern.matcher(key.getFullCoordinate()).matches(), "Invalid character in key: " + key);
      assertTrue(key.toString().length() <= 255, "Key is too long: " + key.toString().length());
    });
  }

  @ParameterizedTest
  @MethodSource(PROVIDE_TEST_CASES)
  void testSplitUpdate(final SecretsTestCase testCase) {
    final var uuidIterator = UUIDS.iterator();
    final var inputPartialConfig = testCase.getPartialConfig();
    final var inputUpdateConfig = testCase.getUpdateConfig();
    final var inputPartialConfigCopy = inputPartialConfig.deepCopy();
    final var inputUpdateConfigCopy = inputUpdateConfig.deepCopy();
    final var secretPersistence = new MemorySecretPersistence();

    for (final Map.Entry<SecretCoordinate, String> entry : testCase.getFirstSecretMap().entrySet()) {
      secretPersistence.write(entry.getKey(), entry.getValue());
    }

    final var updatedSplit = SecretsHelpers.splitAndUpdateConfig(
        uuidIterator::next,
        WORKSPACE_ID,
        inputPartialConfig,
        inputUpdateConfig,
        testCase.getSpec(),
        secretPersistence::read);

    assertEquals(testCase.getUpdatedPartialConfig(), updatedSplit.getPartialConfig());
    assertEquals(testCase.getSecondSecretMap(), updatedSplit.getCoordinateToPayload());

    // check that we didn't mutate the input configs
    assertEquals(inputPartialConfigCopy, inputPartialConfig);
    assertEquals(inputUpdateConfigCopy, inputUpdateConfig);
  }

  @ParameterizedTest
  @MethodSource(PROVIDE_TEST_CASES)
  void testCombine(final SecretsTestCase testCase) {
    final var secretPersistence = new MemorySecretPersistence();
    testCase.getPersistenceUpdater().accept(secretPersistence);

    final var inputPartialConfig = testCase.getPartialConfig();
    final var inputPartialConfigCopy = inputPartialConfig.deepCopy();
    final var actualCombinedConfig = SecretsHelpers.combineConfig(testCase.getPartialConfig(), secretPersistence);

    assertEquals(testCase.getFullConfig(), actualCombinedConfig);

    // check that we didn't mutate the input configs
    assertEquals(inputPartialConfigCopy, inputPartialConfig);
  }

  @Test
  void testMissingSecretShouldThrowException() {
    final var testCase = new SimpleTestCase();
    final var secretPersistence = new MemorySecretPersistence();

    // intentionally do not seed the persistence with
    // testCase.getPersistenceUpdater().accept(secretPersistence);

    assertThrows(RuntimeException.class, () -> SecretsHelpers.combineConfig(testCase.getPartialConfig(), secretPersistence));
  }

  @Test
  void testUpdatingSecretsOneAtATime() {
    final var uuidIterator = UUIDS.iterator();
    final var secretPersistence = new MemorySecretPersistence();
    final var testCase = new NestedObjectTestCase();

    final var splitConfig = SecretsHelpers.splitConfig(
        uuidIterator::next,
        WORKSPACE_ID,
        testCase.getFullConfig(),
        testCase.getSpec());

    assertEquals(testCase.getPartialConfig(), splitConfig.getPartialConfig());
    assertEquals(testCase.getFirstSecretMap(), splitConfig.getCoordinateToPayload());

    for (final Map.Entry<SecretCoordinate, String> entry : splitConfig.getCoordinateToPayload().entrySet()) {
      secretPersistence.write(entry.getKey(), entry.getValue());
    }

    final var updatedSplit1 = SecretsHelpers.splitAndUpdateConfig(
        uuidIterator::next,
        WORKSPACE_ID,
        testCase.getPartialConfig(),
        testCase.getFullConfigUpdate1(),
        testCase.getSpec(),
        secretPersistence::read);

    assertEquals(testCase.getUpdatedPartialConfigAfterUpdate1(), updatedSplit1.getPartialConfig());
    assertEquals(testCase.getSecretMapAfterUpdate1(), updatedSplit1.getCoordinateToPayload());

    for (final Map.Entry<SecretCoordinate, String> entry : updatedSplit1.getCoordinateToPayload().entrySet()) {
      secretPersistence.write(entry.getKey(), entry.getValue());
    }

    final var updatedSplit2 = SecretsHelpers.splitAndUpdateConfig(
        uuidIterator::next,
        WORKSPACE_ID,
        updatedSplit1.getPartialConfig(),
        testCase.getFullConfigUpdate2(),
        testCase.getSpec(),
        secretPersistence::read);

    assertEquals(testCase.getUpdatedPartialConfigAfterUpdate2(), updatedSplit2.getPartialConfig());
    assertEquals(testCase.getSecretMapAfterUpdate2(), updatedSplit2.getCoordinateToPayload());
  }

  @ParameterizedTest
  @MethodSource(PROVIDE_TEST_CASES)
  void testSecretPath(final SecretsTestCase testCase) throws IOException {
    final JsonNode spec = testCase.getSpec().getConnectionSpecification();

    final List<String> secretsPaths = SecretsHelpers.getSortedSecretPaths(spec);

    Assertions.assertThat(secretsPaths).containsExactlyElementsOf(testCase.getExpectedSecretsPaths());
  }

}
