#!/usr/bin/perl

use FindBin;
use lib "$FindBin::Bin/../lib";

use JMAP::Tester;
use JMAP::Validation::Checks::ContactGroup;
use JMAP::Validation::Checks::Error;
use JMAP::Validation::Generators::String;
use JSON::PP;
use Test2::Bundle::Extended;
use Test::Deep qw{eq_deeply};

my (
  %ACCOUNTS,
  $STATE,
  @TESTS,
);

init();
do_tests();
done_testing();

sub _define_error_tests {
  {
    my @accountIds = qw{
      ok
      true false
      negative_int negative_real
      zero
      int real
      array
      object
    };

    my @ids = qw{
      ok
      true false
      negative_int negative_real
      zero
      int real
      string
      object
      array_true array_false
      array_negative_int array_negative_real
      array_zero
      array_int array_real
      array_array array_object
    };

    foreach my $accountId (@accountIds) {
      foreach my $ids (@ids) {
        next if ($accountId eq 'ok') && ($ids eq 'ok');

        push @TESTS, {
          is_error  => 1,
          type      => 'invalidArguments',
          accountId => $accountId,
          ids       => $ids,
        };
      }
    }
  }

  push @TESTS, {
    is_error  => 1,
    type      => 'accountNotFound',
    accountId => JMAP::Validation::Generators::String->generate(),
    ids       => [JMAP::Validation::Generators::String->generate()],
  };

  # TODO: accountNoContacts

}

sub _define_good_tests {
  foreach my $account ('first', 'second', 'default') {
    foreach my $state ('changed', 'unchanged') {
      foreach my $ids ('all', 'even', 'odd', 'none', 'all_not_found', 'some_not_found') {
        push @TESTS, {
          account => $account,
          state   => $state,
          ids     => $ids,
        };
      }
    }
  }
}

sub do_tests {
  _reset_state();

  foreach my $test (@TESTS) {
    my ($jmap, $request_args) = $test->{is_error}
      ? _build_error_request($test)
      : _build_good_request($test);

    my $result = $jmap->request([ ["getContactGroups", $request_args] ])
      or die "Error sending request. Check that the JMAP endpoint is valid\n";

    if ($test->{is_error}) {
      my $error = $result && $result->sentence(0) && $result->sentence(0)->as_struct();

      is($error, $JMAP::Validation::Checks::Error::is_error);
      is($error, _build_error_response($test));

      next;
    }

    my $contactGroups = $result && $result->sentence(0) && $result->sentence(0)->arguments();

    is($contactGroups, $JMAP::Validation::Checks::ContactGroup::is_contactGroups);
    is($contactGroups, _build_good_response($test));
  }
}

sub init {
  unless (scalar(@ARGV) == 2) {
    # TODO: add authentication via access token
    die "usage: $0 <accountId1:jmap-account1-uri> <accountId2:jmap-account2-uri>\n";
  }

  for my $account (qw{first second}) {
    my ($accountId, $uri) = $ARGV[($account eq 'first') ? 0 : 1] =~ /([^:]+):(.*)/;

    unless ($accountId and $uri) {
      die "Parameters are not in the following format <accountId:jmap-account-uri>\n";
    }

    $ACCOUNTS{$account} = {
      accountId      => $accountId,
      jmap           => JMAP::Tester->new({ jmap_uri => $uri }),
      contact_groups => [
        map {
          {
            name       => JMAP::Validation::Generators::String->generate(),
            contactIds => [], # TODO need to create real contact
          }
        } 1..6
      ],
    };
  }

  _define_error_tests();
  _define_good_tests();
}

sub _build_error_request {
  my ($test) = @_;

  my $jmap = $test->{accountNoContact}
      ? $ARGV[2]
      : $ACCOUNTS{first}{jmap};

  my %request_args;

  if ($test->{type} eq 'invalidArguments') {
    %request_args = (
      accountId => {
        true          => JSON::PP::true,
        false         => JSON::PP::false,
        negative_int  => JSON::Typist::Number->new(0-int(rand 2**64)),
        negative_real => JSON::Typist::Number->new(0-(rand 2**64)),
        zero          => JSON::Typist::Number->new(0),
        int           => JSON::Typist::Number->new(int(rand 2**64)),
        real          => JSON::Typist::Number->new(rand 2**64),
        array         => [],
        object        => {},
        ok            => $ACCOUNTS{first}{accountId},
      }->{$test->{accountId} || 'ok'},
      ids => {
        true                => JSON::PP::true,
        false               => JSON::PP::false,
        negative_int        => JSON::Typist::Number->new(0-int(rand 2**64)),
        negative_real       => JSON::Typist::Number->new(0-(rand 2**64)),
        zero                => JSON::Typist::Number->new(0),
        int                 => JSON::Typist::Number->new(int(rand 2**64)),
        real                => JSON::Typist::Number->new(rand 2**64),
        string              => JMAP::Validation::Generators::String->generate(),
        object              => {},
        array_true          => [JSON::PP::true],
        array_false         => [JSON::PP::false],
        array_negative_int  => [JSON::Typist::Number->new(0-int(rand 2**64))],
        array_negative_real => [JSON::Typist::Number->new(0-(rand 2**64))],
        array_zero          => [JSON::Typist::Number->new(0)],
        array_int           => [JSON::Typist::Number->new(int(rand 2**64))],
        array_real          => [JSON::Typist::Number->new(rand 2**64)],
        array_array         => [[]],
        array_object        => [{}],
        ok                  => [JMAP::Validation::Generators::String->generate()],
      }->{$test->{ids} || 'ok'},
    );
  }

  if ($test->{type} eq 'accountNotFound') {
    %request_args = (
      accountId => JMAP::Validation::Generators::String->generate(),
      ids       => [JMAP::Validation::Generators::String->generate()],
    );
  }

  return ($jmap, \%request_args);
}

sub _build_good_request {
  my ($test) = @_;

  my $account = ($test->{account} =~ /first|default/) ? 'first' : 'second';
  my $jmap    = $ACCOUNTS{$account}{jmap};

  my %request_args = (
    accountId => (
      ($test->{account} =~ /first|second/)
        ? $ACCOUNTS{$account}{accountId}
        : JSON::PP::null,
    ),
    ids => {
      all            => JSON::PP::null,
      even           => [map { $_->{id} } @{$STATE->{$account}{contact_groups}}[1, 3, 5]],
      odd            => [map { $_->{id} } @{$STATE->{$account}{contact_groups}}[0, 2, 4]],
      none           => [],
      all_not_found  => [qw{these ids do not exist}],
      some_not_found => [
        (map { $_->{id} } @{$STATE->{$account}{contact_groups}}[2, 5]),
        qw{some ids do not exist}
      ],
    }->{$test->{ids}},
  );

  return ($jmap, \%request_args);
}

sub _build_error_response {
  my ($test) = @_;

  my $response_check = array {
    item 1 => hash {
      field type => string($test->{type})
    };
  };

  return $response_check;
}

sub _build_good_response {
  my ($test) = @_;

  my $account   = ($test->{account} =~ /first|default/) ? 'first' : 'second';
  my $accountId = JSON::Typist::String->new($ACCOUNTS{$account}{accountId});

  my $response_check = hash {
    field accountId => string($accountId);

    # TODO: state

    my $list = {
      all            => [@{$STATE->{$account}{contact_groups}}[0..5]],
      even           => [@{$STATE->{$account}{contact_groups}}[1, 3, 5]],
      odd            => [@{$STATE->{$account}{contact_groups}}[0, 2, 4]],
      none           => [],
      all_not_found  => [],
      some_not_found => [@{$STATE->{$account}{contact_groups}}[2, 5]],
    }->{$test->{ids}};

    field list => validator(sub {
      my (%params) = @_;

      return eq_deeply(
        [sort { $a->{id} cmp $b->{id} } @{$list        || []}],
        [sort { $a->{id} cmp $b->{id} } @{$params{got} || []}]
      );
    });

    my $notFound = {
      all            => JSON::PP::null,
      even           => JSON::PP::null,
      odd            => JSON::PP::null,
      none           => JSON::PP::null,
      all_not_found  => [map { JSON::Typist::String->new($_) } qw{these ids do not exist}],
      some_not_found => [map { JSON::Typist::String->new($_) } qw{some ids do not exist}],
    }->{$test->{ids}};

    field notFound => validator(sub {
      my (%params) = @_;

      return eq_deeply(
        [sort @{$notFound    || []}],
        [sort @{$params{got} || []}]
      );
    });
  };

  return $response_check;
}

sub _reset_state {
  $STATE = {};

  my $creation_id = 0;

  foreach my $account (keys %ACCOUNTS) {
    my $contactGroups = $ACCOUNTS{$account}{jmap}->request([ ["getContactGroups", {} ] ])
      or die "Error getting contact groups for '$account'";

    $ACCOUNTS{$account}{jmap}->request([
      [
        "setContactGroups",
        {
          create  => {},
          update  => {},
          destroy => [ map { $_->{id} } @{$contactGroups->sentence(0)->arguments()->{list} || []}],
        },
      ],
    ]) or die "Error deleting contact groups for '$account'";

    $ACCOUNTS{$account}{jmap}->request([
      [
        "setContactGroups",
        {
          create  => { map { $creation_id++ => $_ } @{$ACCOUNTS{$account}{contact_groups}} },
          update  => {},
          destroy => [],
        },
      ],
    ]) or die "Error creating contact groups for '$account'\n";

    $contactGroups = $ACCOUNTS{$account}{jmap}->request([ ["getContactGroups", {} ] ])
      or die "Error getting contact groups for '$account'";

    my %keyed_contactGroups
      = map { $_->{name} => $_ }
          @{$contactGroups->sentence(0)->arguments()->{list} || []};

    foreach my $contact_group (@{$ACCOUNTS{$account}{contact_groups}}) {
      die "Error getting contact group '$contact_group->{name}'\n"
        unless exists $keyed_contactGroups{$contact_group->{name}};

      push @{$STATE->{$account}{contact_groups}}, $keyed_contactGroups{$contact_group->{name}};
    }
  }
}
