use strict;
use warnings;

use lib qw{ . lib};

use Data::Dumper;
use Date::Format;
use English qw{ -no_match_vars };
use JSON::PP;
use MIME::Base64;
use Test::More;
use UnitTestSetup qw(:all);

BEGIN {
  my $has_crypt_cbc = eval {
    require Crypt::CBC;
    require Crypt::Cipher::AES;
  };

  if ( !$has_crypt_cbc ) {
    plan skip_all => 'Crypt::CBC unavilable';
  }
  else {
    plan tests => 14;
  }

  {
    no strict 'refs';  ## no critic

    *{'HTTP::Request::new'}     = sub { bless {}, 'HTTP::Request'; };
    *{'HTTP::Request::request'} = sub { HTTP::Response->new; };

    *{'HTTP::Response::new'}        = sub { bless {}, 'HTTP::Response'; };
    *{'HTTP::Response::is_success'} = sub { 1; };

    *{'LWP::UserAgent::new'}     = sub { bless {}, 'LWP::UserAgent'; };
    *{'LWP::UserAgent::request'} = sub { HTTP::Response->new; };
    ## no critic
  }

  use Module::Loaded;

  mark_as_loaded(HTTP::Request);
  mark_as_loaded(HTTP::Response);
  mark_as_loaded(LWP::UserAgent);

  use_ok('Amazon::Credentials');
}

########################################################################
sub my_encrypt {
########################################################################
  my ( $str, $passkey ) = @_;
  return if !$str;

  my $sum = 0;

  foreach ( split //xsm, $passkey ) {
    $sum += ord $_;
  }

  my @encrypted_str;
  foreach ( split //xsm, $str ) {
    push @encrypted_str, $sum + ord $_;
  }

  return \@encrypted_str;
}

########################################################################
sub my_decrypt {
########################################################################
  my ( $str, $passkey ) = @_;
  return if !$str;

  my @encrypted_str = @{$str};
  my $sum           = 0;

  foreach ( split //xsm, $passkey ) {
    $sum += ord $_;
  }

  $str = q{};

  foreach my $c (@encrypted_str) {
    $c -= $sum;
    $str .= chr $c;
  }

  return $str;
}

########################################################################
sub check_credentials {
########################################################################
  my ( $credentials, $unencrypted_creds, $test ) = @_;

  $test = $test // q{};

  my $retval = 0;

  if ( $credentials->get_cache ) {
    foreach my $e (qw{ access_key_id secret_access_key }) {
      my $encrypted_value = $credentials->can( 'get__' . $e )->($credentials);
      my $unencrypted_value
        = $credentials->can( 'get_aws_' . $e )->($credentials);

      $retval
        += !ok( $encrypted_value && $encrypted_value ne $unencrypted_value,
        $test . ' - ' . $e . ' encrypted ok' );

      $retval += !ok(
        $unencrypted_value eq $unencrypted_creds->{$e},
        $test . ' - ' . $e . ' decrypted ok'
      );

    }
  }
  else {
    foreach my $e (qw{access_key_id secret_access_key }) {
      $retval
        += !ok( !defined $credentials->can( 'get__' . $e )->($credentials),
        $test . ' - ' . $e . ' not cached' );
    }
  }

  return !$retval;
}

########################################################################
sub check_cipher {
########################################################################
  my ( $cipher_name, $test ) = @_;

  my $credentials = Amazon::Credentials->new(
    profile            => 'foo',
    cipher             => $cipher_name,
    no_passkey_warning => 1,
  );

  ok( $credentials->get_encryption, 'encryption enabled' );

  $cipher_name = $cipher_name || $credentials->get_cipher;

  is( $credentials->get_cipher, $cipher_name, $test || $cipher_name )
    or diag( $credentials->get_cipher );

  my $passkey = $credentials->get_passkey;

  my $cipher = Crypt::CBC->new(
    '-pass'        => $passkey,
    '-key'         => $passkey,
    '-nodeprecate' => 1,
    '-cipher'      => $cipher_name,
  );

  my $access_key_id = decode_base64( $credentials->get__access_key_id );
  my $unencrypted_access_key_id = $credentials->get_aws_access_key_id;

  my $encrypted_access_key_id = $cipher->encrypt($unencrypted_access_key_id);

  isnt( $encrypted_access_key_id, $access_key_id,
    'encrypted strings different (salt)' )
    or diag( Dumper [ $passkey, $encrypted_access_key_id, $access_key_id ] );

  # decrypt your encrypted string with my cipher
  is(
    $cipher->decrypt($access_key_id),
    $credentials->get_aws_access_key_id,
    'encrypted with ' . $cipher_name
    )
    or diag( Dumper [ $passkey, $encrypted_access_key_id, $access_key_id ] );

  return;
}

# +------------------ +
# | TESTS START HERE |
# +------------------ +

init_test;

Amazon::Credentials->import('create_passkey');

my %unencrypted_creds = (
  access_key_id     => 'foo-aws-access-key-id',
  secret_access_key => 'foo-aws-secret-access-key',
);

# !! this test must be run first !!
########################################################################
subtest 'obfuscation without Crypt::CBC' => sub {
########################################################################

  {
    # use Devel::Hide qw{ -lexically  -quiet Crypt::CBC };
    eval "use Test::Without::Module qw{ Crypt::CBC Crypt::Cipher::AES };";  ## no critic

    my $credentials = Amazon::Credentials->new(
      profile            => 'foo',
      encryption         => 1,
      no_passkey_warning => 1,
    );

    ok( !$credentials->get_encryption,
      'encryption disabled (no Crypt::CBC)' );

    ok(
      decode_base64( $credentials->get__access_key_id ) eq
        $unencrypted_creds{access_key_id},
      'base64 encoded obfuscation'
    );

    ok(
      decode_base64( $credentials->get__secret_access_key ) eq
        $unencrypted_creds{secret_access_key},
      'base64 encoded obfuscation'
    );

    check_credentials( $credentials, \%unencrypted_creds, 'obfuscation' )
      or diag( Dumper [$credentials] );
  }

  eval q{ no Test::Without::Module qw{ Crypt::CBC Crypt::Cipher::AES }; };  ## no critic
};

########################################################################
subtest 'decrypt' => sub {
########################################################################

  my $credentials = Amazon::Credentials->new(
    profile            => 'foo',
    no_passkey_warning => 1,
  );

  ok( defined $credentials->get_passkey, 'passkey created' );

  ok( $credentials->get_encryption, 'default is encryption enabled' );

  check_credentials( $credentials, \%unencrypted_creds, 'decrypt' )
    or diag( Dumper [$credentials] );
};

########################################################################
subtest 'rotate credentials' => sub {
########################################################################

  my $credentials = Amazon::Credentials->new(
    profile            => 'foo',
    encryption         => 1,
    no_passkey_warning => 1,
  );

  my $passkey     = $credentials->get_passkey;
  my $new_passkey = $credentials->rotate_credentials;

  ok( $new_passkey ne $passkey, 'passkey changed' )
    or diag( Dumper [ $passkey, $new_passkey ] );

  check_credentials( $credentials, \%unencrypted_creds, 'rotate' )
    or diag(
    Dumper [
      $credentials->get_passkey(), 'new:', $new_passkey, $credentials
    ]
    );
};

########################################################################
subtest 'rotate credentials with custom passkey' => sub {
########################################################################

  our $PASSKEY = create_passkey();

  sub get_passkey {
    my ($regenerate) = @_;

    return $regenerate ? create_passkey() : $PASSKEY;
  }

  my $credentials = Amazon::Credentials->new(
    passkey            => \&get_passkey,
    profile            => 'foo',
    cache              => 1,
    no_passkey_warning => 1,
  );

  isa_ok( $credentials, 'Amazon::Credentials' );

  my $old_passkey = $PASSKEY;

  $PASSKEY = $credentials->rotate_credentials( get_passkey(1) );

  ok( $old_passkey && $PASSKEY, 'passkeys are not null' );

  ok( $old_passkey ne $PASSKEY, 'passkey has changed' );

  check_credentials( $credentials, \%unencrypted_creds, 'rotate (cache on)' )
    or diag( Dumper [$credentials] );

  $credentials = Amazon::Credentials->new(
    cache              => 0,
    passkey            => \&get_passkey,
    profile            => 'foo',
    no_passkey_warning => 1,
  );

  $old_passkey = $PASSKEY = get_passkey(1);

  $PASSKEY = $credentials->rotate_credentials( get_passkey(1) );

  ok( $old_passkey && $PASSKEY, 'passkeys are not null (cacheing off)' );

  ok( $old_passkey ne $PASSKEY, 'passkey has changed (cacheing off' );

  check_credentials( $credentials, \%unencrypted_creds, 'rotate (cache off)' )
    or diag( Dumper [$credentials] );

  sub get_passkey_v2 {
    return 'abra cadabra ala kazam!';
  }

  $credentials->set_cache(1);
  $credentials->set_passkey( \&get_passkey_v2 );

  $credentials->reset_credentials(1);

  check_credentials( $credentials, \%unencrypted_creds, 'set new passkey' )
    or diag( Dumper [$credentials] );

  $credentials->set_insecure(1);

  $credentials->set_cache(1);

  $credentials->set_passkey( \&get_passkey );

  $credentials->reset_credentials(1);

  check_credentials( $credentials, \%unencrypted_creds,
    'set new passkey (cached)' )
    or diag( Dumper [$credentials] );
};

########################################################################
subtest 'custom encryption/decryption' => sub {
########################################################################

  my $credentials = Amazon::Credentials->new(
    profile            => 'foo',
    encrypt            => \&my_encrypt,
    decrypt            => \&my_decrypt,
    passkey            => sub { return 'my passkey' },
    no_passkey_warning => 1,
  );

  check_credentials( $credentials, \%unencrypted_creds, 'custom encryption' )
    or diag( Dumper [$credentials] );
};

########################################################################
subtest 'custom encryption/decryption setting' => sub {
########################################################################

  # set only decrypt or encrypt
  foreach my $sub (qw{ encrypt decrypt }) {
    my $credentials = eval {
      return Amazon::Credentials->new(
        profile            => 'foo',
        $sub               => sub { },
        passkey            => sub { return 'my passkey' },
        no_passkey_warning => 1,
      );
    };

    ok( $EVAL_ERROR && $EVAL_ERROR =~ /must\sbe\sa\scode\sref/xsm,
      "set just $sub" )
      or diag($EVAL_ERROR);
  }
};

########################################################################
subtest 'cache credentials' => sub {
########################################################################

  my $credentials = eval {
    return Amazon::Credentials->new(
      profile            => 'foo',
      cache              => 1,
      no_passkey_warning => 1,
    );
  };

  check_credentials( $credentials, \%unencrypted_creds, 'cache on' )
    or diag( Dumper [$credentials] );

  ok( defined $credentials->get__secret_access_key,
    'secret access key retained' );
  ok( defined $credentials->get__access_key_id, 'access key id retained' );

};

########################################################################
subtest 'do not cache credentials' => sub {
########################################################################

  my $credentials = eval {
    return Amazon::Credentials->new(
      profile            => 'foo',
      cache              => 0,
      no_passkey_warning => 1,
    );
  };

  check_credentials( $credentials, \%unencrypted_creds, 'cache off' )
    or diag( Dumper [$credentials] );

  ok( !defined $credentials->get__secret_access_key,
    'secret access key removed' );
  ok( !defined $credentials->get__access_key_id, 'access key id removed' );

};

########################################################################
subtest 'get passkey from sub' => sub {
########################################################################

  my $passkey = create_passkey();

  my $credentials = eval {
    return Amazon::Credentials->new(
      profile    => 'foo',
      cache      => 1,
      encryption => 1,
      passkey    => sub {
        return $passkey;
      },
      no_passkey_warning => 1,
    );
  };

  ok( $credentials->get_encryption, 'encryption enabled' )
    or diag( Dumper [$credentials] );

  check_credentials( $credentials, \%unencrypted_creds, )
    or diag( Dumper [ $passkey, $credentials->get_passkey, $credentials ] );

};

########################################################################
subtest 'rotate credentials w/new passkey' => sub {
########################################################################

  my $passkey = 'abra cadabra ala kazam!';

  my $credentials = eval {
    return Amazon::Credentials->new(
      profile            => 'foo',
      cache              => 1,
      encryption         => 1,
      passkey            => $passkey,
      no_passkey_warning => 1,
    );
  };

  # encrypted values
  my ( $secret_access_key, $access_key_id ) = (
    $credentials->get__secret_access_key,
    $credentials->get__access_key_id
  );

  my $new_passkey = $credentials->rotate_credentials( create_passkey() );

  ok( $new_passkey ne $passkey, 'passkey rotated' )
    or diag(
    Dumper [ $new_passkey, $secret_access_key, $access_key_id, $credentials ]
    );

  check_credentials( $credentials, \%unencrypted_creds )
    or diag(
    Dumper [ $new_passkey, $secret_access_key, $access_key_id, $credentials ]
    );

  ok( $secret_access_key ne $credentials->get__secret_access_key,
    'encrypted secret different' )
    or diag(
    Dumper [ $new_passkey, $secret_access_key, $access_key_id, $credentials ]
    );

  ok( $access_key_id ne $credentials->get__access_key_id,
    'encrypted access_key_id different' )
    or diag(
    Dumper [ $new_passkey, $secret_access_key, $access_key_id, $credentials ]
    );

  $new_passkey = $credentials->rotate_credentials;

  ok( $new_passkey ne $passkey, 'passkey rotated' )
    or diag(
    Dumper [ $new_passkey, $secret_access_key, $access_key_id, $credentials ]
    );

};

########################################################################
subtest 'token encryption' => sub {
########################################################################

  my $credentials = Amazon::Credentials->new(
    aws_access_key_id     => 'foo',
    aws_secret_access_key => 'bar',
    token                 => 'biz',
    encryption            => 1,
    cache                 => 1,
    no_passkey_warning    => 1,
  );

  ok( $credentials->get_encryption, 'encryption enabled' )
    or diag( Dumper [$credentials] );

  ok( $credentials->get__session_token ne 'biz', 'token encrypted' )
    or diag( Dumper [$credentials] );

  ok( $credentials->get_token eq 'biz', 'token decrypted' )
    or diag( Dumper [$credentials] );

  ok( decode_base64( $credentials->get__session_token ) ne 'biz',
    'encrypted, not just obfuscated' )
    or diag( Dumper [$credentials] );
};

########################################################################
subtest 'use Crypt::CBC' => sub {
########################################################################

  eval {
    require Crypt::CBC;
    require Crypt::Cipher::AES;
  };

  if ($EVAL_ERROR) {
    plan skip_all => $EVAL_ERROR;
  }

  check_cipher( q{}, 'default cipher' );
};

########################################################################
subtest 'use custom cipher' => sub {
########################################################################

  my $cipher_name = $ENV{AMAZON_CREDENTIAL_TEST_CIPHER} || 'Crypt::Blowfish';

  eval "require $cipher_name;";  ## no critic

  if ($EVAL_ERROR) {
    plan skip_all => $EVAL_ERROR;
  }

  check_cipher( $cipher_name, 'custom cipher ' . $cipher_name );
};

1;
