package com.oblador.keychain;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;

import androidx.annotation.NonNull;
import androidx.biometric.BiometricManager;
import androidx.test.core.app.ApplicationProvider;

import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.oblador.keychain.KeychainModule.AccessControl;
import com.oblador.keychain.KeychainModule.Errors;
import com.oblador.keychain.KeychainModule.KnownCiphers;
import com.oblador.keychain.KeychainModule.Maps;
import com.oblador.keychain.cipherStorage.CipherStorage;
import com.oblador.keychain.cipherStorage.CipherStorageBase;
import com.oblador.keychain.cipherStorage.CipherStorageFacebookConceal;
import com.oblador.keychain.cipherStorage.CipherStorageKeystoreAesCbc;
import com.oblador.keychain.cipherStorage.CipherStorageKeystoreRsaEcb;
import com.oblador.keychain.exceptions.CryptoFailedException;
import com.oblador.keychain.exceptions.KeyStoreAccessException;

import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.rules.Timeout;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.junit.VerificationCollector;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

import java.security.KeyStore;
import java.security.Security;

import javax.crypto.Cipher;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;

@RunWith(RobolectricTestRunner.class)
public class KeychainModuleTests {
  public static final byte[] BYTES_USERNAME = "username".getBytes();
  public static final byte[] BYTES_PASSWORD = "password".getBytes();
  /**
   * Cancel test after 5 seconds.
   */
  @ClassRule
  public static Timeout timeout = Timeout.seconds(10);
  /**
   * Get test method name.
   */
  @Rule
  public TestName methodName = new TestName();
  /**
   * Mock all the dependencies.
   */
  @Rule
  public MockitoRule mockDependencies = MockitoJUnit.rule().silent();
  @Rule
  public VerificationCollector collector = MockitoJUnit.collector();
  /**
   * Security fake provider.
   */
  private FakeProvider provider = new FakeProvider();

  @Before
  public void setUp() throws Exception {
    provider.configuration.clear();

    Security.insertProviderAt(provider, 0);
  }

  @After
  public void tearDown() throws Exception {
    Security.removeProvider(FakeProvider.NAME);
  }

  @NonNull
  private ReactApplicationContext getRNContext() {
    return new ReactApplicationContext(ApplicationProvider.getApplicationContext());
  }

  @Test
  @Config(sdk = Build.VERSION_CODES.LOLLIPOP)
  public void testFingerprintNoHardware_api21() throws Exception {
    // GIVEN: API21 android version
    ReactApplicationContext context = getRNContext();
    KeychainModule module = new KeychainModule(context);

    // WHEN: verify availability
    final int result = BiometricManager.from(context).canAuthenticate();
    final boolean isFingerprintAvailable = module.isFingerprintAuthAvailable();

    // THEN: in api lower 23 - biometric is not available at all
    assertThat(isFingerprintAvailable, is(false));
    assertThat(result, is(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE));

    // fingerprint hardware not available, minimal API for fingerprint is api23, Android 6.0
    // https://developer.android.com/about/versions/marshmallow/android-6.0
  }

  @Test
  @Config(sdk = Build.VERSION_CODES.M)
  public void testFingerprintAvailableButNotConfigured_api23() throws Exception {
    // GIVEN:
    //   fingerprint api available but not configured properly
    //   API23 android version
    ReactApplicationContext context = getRNContext();
    KeychainModule module = new KeychainModule(context);

    // set that hardware is available
    FingerprintManager fm = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
    shadowOf(fm).setIsHardwareDetected(true);

    // WHEN: check availability
    final int result = BiometricManager.from(context).canAuthenticate();
    final boolean isFingerprintWorking = module.isFingerprintAuthAvailable();

    // THEN: another status from biometric api, fingerprint is still unavailable
    assertThat(result, is(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED));
    assertThat(isFingerprintWorking, is(false));
  }

  @Test
  @Config(sdk = Build.VERSION_CODES.M)
  public void testFingerprintConfigured_api23() throws Exception {
    // GIVEN:
    //   API23 android version
    //   Fingerprints are configured
    //   fingerprint feature is ignored by android os
    ReactApplicationContext context = getRNContext();

    // set that hardware is available
    FingerprintManager fm = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
    shadowOf(fm).setIsHardwareDetected(true);
    shadowOf(fm).setDefaultFingerprints(5); // 5 fingerprints are available

    // WHEN: check availability
    final int result = BiometricManager.from(context).canAuthenticate();
    final KeychainModule module = new KeychainModule(context);
    final boolean isFingerprintWorking = module.isFingerprintAuthAvailable();

    // THEN: biometric works
    assertThat(result, is(BiometricManager.BIOMETRIC_SUCCESS));
    assertThat(isFingerprintWorking, is(true));
  }

  @Test
  @Config(sdk = Build.VERSION_CODES.P)
  public void testFingerprintConfigured_api28() throws Exception {
    // GIVEN:
    //   API28 android version
    //   for api24+ system feature should be enabled
    //   fingerprints are configured
    ReactApplicationContext context = getRNContext();
    shadowOf(context.getPackageManager()).setSystemFeature(PackageManager.FEATURE_FINGERPRINT, true);

    // set that hardware is available
    FingerprintManager fm = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
    shadowOf(fm).setIsHardwareDetected(true);
    shadowOf(fm).setDefaultFingerprints(5); // 5 fingerprints are available

    // WHEN: verify availability
    final int result = BiometricManager.from(context).canAuthenticate();
    final KeychainModule module = new KeychainModule(context);
    final boolean isFingerprintWorking = module.isFingerprintAuthAvailable();

    // THEN: biometrics works
    assertThat(result, is(BiometricManager.BIOMETRIC_SUCCESS));
    assertThat(isFingerprintWorking, is(true));
  }

  @Test
  @Config(sdk = Build.VERSION_CODES.KITKAT)
  public void testExtractFacebookConceal_NoHardware_api19() throws Exception {
    // GIVEN:
    //  API19, minimal Android version
    final ReactApplicationContext context = getRNContext();

    // WHEN: ask keychain for secured storage
    final KeychainModule module = new KeychainModule(context);
    final CipherStorage storage = module.getCipherStorageForCurrentAPILevel();

    // THEN: expected Facebook cipher storage, its the only one that supports API19
    assertThat(storage, notNullValue());
    assertThat(storage, instanceOf(CipherStorageFacebookConceal.class));
    assertThat(storage.isBiometrySupported(), is(false));
    assertThat(storage.securityLevel(), is(SecurityLevel.ANY));
    assertThat(storage.getMinSupportedApiLevel(), is(Build.VERSION_CODES.JELLY_BEAN));
    assertThat(storage.supportsSecureHardware(), is(false));
  }

  @Test
  @Config(sdk = Build.VERSION_CODES.M)
  public void testExtractAesCbc_NoFingerprintConfigured_api23() throws Exception {
    // GIVEN:
    //  API23 android version
    final ReactApplicationContext context = getRNContext();

    // WHEN: get the best secured storage
    final KeychainModule module = new KeychainModule(context);
    final CipherStorage storage = module.getCipherStorageForCurrentAPILevel();

    // THEN:
    //   expected AES cipher storage due no fingerprint available
    //   AES win and returned instead of facebook cipher
    assertThat(storage, notNullValue());
    assertThat(storage, instanceOf(CipherStorageKeystoreAesCbc.class));
    assertThat(storage.isBiometrySupported(), is(false));
    assertThat(storage.securityLevel(), is(SecurityLevel.SECURE_HARDWARE));
    assertThat(storage.getMinSupportedApiLevel(), is(Build.VERSION_CODES.M));
    assertThat(storage.supportsSecureHardware(), is(true));
  }

  @Test
  @Config(sdk = Build.VERSION_CODES.M)
  public void testExtractRsaEcb_EnabledFingerprint_api23() throws Exception {
    // GIVEN:
    //   API23 android version
    //   fingerprints configured
    final ReactApplicationContext context = getRNContext();

    // set that hardware is available and fingerprints configured
    final FingerprintManager fm = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
    shadowOf(fm).setIsHardwareDetected(true);
    shadowOf(fm).setDefaultFingerprints(5); // 5 fingerprints are available

    // WHEN: fingerprint availability influence on storage selection
    final KeychainModule module = new KeychainModule(context);
    final boolean isFingerprintWorking = module.isFingerprintAuthAvailable();
    final CipherStorage storage = module.getCipherStorageForCurrentAPILevel();

    // THEN: expected RsaEcb with working fingerprint
    assertThat(isFingerprintWorking, is(true));
    assertThat(storage, notNullValue());
    assertThat(storage, instanceOf(CipherStorageKeystoreRsaEcb.class));
    assertThat(storage.isBiometrySupported(), is(true));
    assertThat(storage.securityLevel(), is(SecurityLevel.SECURE_HARDWARE));
    assertThat(storage.getMinSupportedApiLevel(), is(Build.VERSION_CODES.M));
    assertThat(storage.supportsSecureHardware(), is(true));
  }

  @Test
  @Config(sdk = Build.VERSION_CODES.P)
  public void testExtractRsaEcb_EnabledFingerprint_api28() throws Exception {
    // GIVEN:
    //   API28 android version
    //   fingerprint feature enabled
    //   fingerprints configured
    final ReactApplicationContext context = getRNContext();
    shadowOf(context.getPackageManager()).setSystemFeature(PackageManager.FEATURE_FINGERPRINT, true);

    // set that hardware is available and fingerprints configured
    final FingerprintManager fm = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
    shadowOf(fm).setIsHardwareDetected(true);
    shadowOf(fm).setDefaultFingerprints(5); // 5 fingerprints are available

    // WHEN: get secured storage
    final int result = BiometricManager.from(context).canAuthenticate();
    final KeychainModule module = new KeychainModule(context);
    final boolean isFingerprintWorking = module.isFingerprintAuthAvailable();
    final CipherStorage storage = module.getCipherStorageForCurrentAPILevel();

    // THEN: expected RsaEcb with working fingerprint
    assertThat(isFingerprintWorking, is(true));
    assertThat(result, is(BiometricManager.BIOMETRIC_SUCCESS));
    assertThat(storage, notNullValue());
    assertThat(storage, instanceOf(CipherStorageKeystoreRsaEcb.class));
    assertThat(storage.isBiometrySupported(), is(true));
    assertThat(storage.securityLevel(), is(SecurityLevel.SECURE_HARDWARE));
    assertThat(storage.getMinSupportedApiLevel(), is(Build.VERSION_CODES.M));
    assertThat(storage.supportsSecureHardware(), is(true));
  }

  @Test
  @Config(sdk = Build.VERSION_CODES.M)
  public void testMigrateStorageFromOlder_api23() throws Exception {
    // GIVEN:
    final ReactApplicationContext context = getRNContext();
    final CipherStorage aes = Mockito.mock(CipherStorage.class);
    final CipherStorage rsa = Mockito.mock(CipherStorage.class);
    when(rsa.getCipherStorageName()).thenReturn("dummy");

    final CipherStorage.DecryptionResult decrypted = new CipherStorage.DecryptionResult("user", "password");
    final CipherStorage.EncryptionResult encrypted = new CipherStorage.EncryptionResult("user".getBytes(), "password".getBytes(), rsa);
    final KeychainModule module = new KeychainModule(context);
    final SharedPreferences prefs = context.getSharedPreferences(PrefsStorage.KEYCHAIN_DATA, Context.MODE_PRIVATE);

    when(
      rsa.encrypt(eq("dummy"), eq("user"), eq("password"), any())
    ).thenReturn(encrypted);

    // WHEN:
    module.migrateCipherStorage("dummy", rsa, aes, decrypted);
    final String username = prefs.getString(PrefsStorage.getKeyForUsername("dummy"), "");
    final String password = prefs.getString(PrefsStorage.getKeyForPassword("dummy"), "");
    final String cipherName = prefs.getString(PrefsStorage.getKeyForCipherStorage("dummy"), "");

    // THEN:
    //   delete of key from old storage
    //   re-store of encrypted data in shared preferences
    verify(rsa).encrypt("dummy", "user", "password", SecurityLevel.ANY);
    verify(aes).removeKey("dummy");

    // Base64.DEFAULT force '\n' char in the end of string
    assertThat(username, is("dXNlcg==\n"));
    assertThat(password, is("cGFzc3dvcmQ=\n"));
    assertThat(cipherName, is("dummy"));
  }

  @Test
  @Config(sdk = Build.VERSION_CODES.P)
  public void testGetSecurityLevel_Unspecified_api28() throws Exception {
    // GIVE:
    final ReactApplicationContext context = getRNContext();
    final KeychainModule module = new KeychainModule(context);
    final Promise mockPromise = mock(Promise.class);

    // WHEN:
    module.getSecurityLevel(null, mockPromise);

    // THEN:
    verify(mockPromise).resolve(SecurityLevel.SECURE_HARDWARE.name());
  }

  @Test
  @Config(sdk = Build.VERSION_CODES.M)
  public void testGetSecurityLevel_Unspecified_api23() throws Exception {
    // GIVE:
    final ReactApplicationContext context = getRNContext();
    final KeychainModule module = new KeychainModule(context);
    final Promise mockPromise = mock(Promise.class);

    // WHEN:
    module.getSecurityLevel(null, mockPromise);

    // THEN:
    verify(mockPromise).resolve(SecurityLevel.SECURE_HARDWARE.name());
  }

  @Test
  @Config(sdk = Build.VERSION_CODES.LOLLIPOP)
  public void testGetSecurityLevel_Unspecified_api21() throws Exception {
    // GIVE:
    final ReactApplicationContext context = getRNContext();
    final KeychainModule module = new KeychainModule(context);
    final Promise mockPromise = mock(Promise.class);

    // WHEN:
    module.getSecurityLevel(null, mockPromise);

    // THEN:
    verify(mockPromise).resolve(SecurityLevel.ANY.name());
  }

  @Test
  @Config(sdk = Build.VERSION_CODES.KITKAT)
  public void testGetSecurityLevel_Unspecified_api19() throws Exception {
    // GIVE:
    final ReactApplicationContext context = getRNContext();
    final KeychainModule module = new KeychainModule(context);
    final Promise mockPromise = mock(Promise.class);

    // WHEN:
    module.getSecurityLevel(null, mockPromise);

    // THEN:
    verify(mockPromise).resolve(SecurityLevel.ANY.name());
  }

  @Test
  @Config(sdk = Build.VERSION_CODES.P)
  public void testGetSecurityLevel_NoBiometry_api28() throws Exception {
    // GIVE:
    final ReactApplicationContext context = getRNContext();
    final KeychainModule module = new KeychainModule(context);
    final Promise mockPromise = mock(Promise.class);

    // WHEN:
    final JavaOnlyMap options = new JavaOnlyMap();
    options.putString(Maps.ACCESS_CONTROL, AccessControl.DEVICE_PASSCODE);

    module.getSecurityLevel(options, mockPromise);

    // THEN:
    verify(mockPromise).resolve(SecurityLevel.SECURE_HARDWARE.name());
  }

  @Test
  @Config(sdk = Build.VERSION_CODES.P)
  public void testGetSecurityLevel_NoBiometry_NoSecuredHardware_api28() throws Exception {
    // GIVE:
    final ReactApplicationContext context = getRNContext();
    final KeychainModule module = new KeychainModule(context);
    final Promise mockPromise = mock(Promise.class);

    // set key info - software method
    provider.configuration.put("isInsideSecureHardware", false);

    // WHEN:
    final JavaOnlyMap options = new JavaOnlyMap();
    options.putString(Maps.ACCESS_CONTROL, AccessControl.DEVICE_PASSCODE);

    module.getSecurityLevel(options, mockPromise);

    // THEN:
    // expected AesCbc usage
    assertThat(provider.mocks.get("KeyGenerator"), notNullValue());
    assertThat(provider.mocks.get("KeyGenerator").get("AES"), notNullValue());
    assertThat(provider.mocks.get("KeyPairGenerator"), notNullValue());
    assertThat(provider.mocks.get("KeyPairGenerator").get("RSA"), notNullValue());
    verify(mockPromise).resolve(SecurityLevel.SECURE_SOFTWARE.name());
  }

  @Test
  @Config(sdk = Build.VERSION_CODES.P)
  public void testDowngradeBiometricToAes_api28() throws Exception {
    // GIVEN:
    final ReactApplicationContext context = getRNContext();
    final KeychainModule module = new KeychainModule(context);
    final PrefsStorage prefs = new PrefsStorage(context);
    final Cipher mockCipher = Mockito.mock(Cipher.class);
    final KeyStore mockKeyStore = Mockito.mock(KeyStore.class);
    final CipherStorage storage = module.getCipherStorageByName(KnownCiphers.RSA);
    final CipherStorage.EncryptionResult result = new CipherStorage.EncryptionResult(BYTES_USERNAME, BYTES_PASSWORD, storage);
    final Promise mockPromise = mock(Promise.class);
    final JavaOnlyMap options = new JavaOnlyMap();
    options.putString(Maps.SERVICE, "dummy");

    // store record done with RSA/Biometric cipher
    prefs.storeEncryptedEntry("dummy", result);

    assertThat(storage, instanceOf(CipherStorage.class));
    ((CipherStorageBase)storage).setCipher(mockCipher).setKeyStore(mockKeyStore);
    when(mockKeyStore.getKey(eq("dummy"), isNull())).thenReturn(null); // return empty Key!

    // WHEN:
    module.getGenericPasswordForOptions(options, mockPromise);

    // THEN:
    ArgumentCaptor<Exception> exception = ArgumentCaptor.forClass(Exception.class);
    verify(mockPromise).reject(eq(Errors.E_CRYPTO_FAILED), exception.capture());
    assertThat(exception.getValue(), instanceOf(CryptoFailedException.class));
    assertThat(exception.getValue().getCause(), instanceOf(KeyStoreAccessException.class));
    assertThat(exception.getValue().getMessage(), is("Wrapped error: Empty key extracted!"));
  }
}