use strict;
use warnings;

use Scalar::Util qw(looks_like_number);

#use Sipwise::Base; #causes segfault when creating threads..
use Net::Domain qw(hostfqdn);
use JSON qw();
use Test::More;
use DateTime::Format::Strptime;
use DateTime::Format::ISO8601;
use Data::Dumper;
use Storable;

use JSON::PP;
use LWP::Debug;

my $is_local_env = 0;

use Config::General;
my $catalyst_config;
if ($is_local_env) {
    $catalyst_config = Config::General->new("../../etc/ngcp_panel.conf");
} else {
    #taken 1:1 from /lib/NGCP/Panel.pm
    my $panel_config;
    for my $path(qw#/etc/ngcp-panel/ngcp_panel.conf etc/ngcp_panel.conf ngcp_panel.conf#) {
        if(-f $path) {
            $panel_config = $path;
            last;
        }
    }
    $panel_config //= 'etc/ngcp_panel.conf';
    $catalyst_config = Config::General->new($panel_config);
}
my %config = $catalyst_config->getall();

my $uri = $ENV{CATALYST_SERVER} || ('https://'.hostfqdn.':4443');

my ($ua, $req, $res);

use Test::Collection;
$ua = Test::Collection->new()->ua();

my $t = time;
my $default_reseller_id = 1;

$req = HTTP::Request->new('POST', $uri.'/api/customercontacts/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
    firstname => "cust_contact_first",
    lastname  => "cust_contact_last",
    email     => "cust_contact\@custcontact.invalid",
    reseller_id => $default_reseller_id,
}));
$res = $ua->request($req);
is($res->code, 201, "create customer contact");
$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
$res = $ua->request($req);
is($res->code, 200, "fetch customer contact");
my $custcontact = JSON::from_json($res->decoded_content);

$req = HTTP::Request->new('POST', $uri.'/api/domains/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
    domain => 'test' . ($t-1) . '.example.org',
    reseller_id => $default_reseller_id,
}));
$res = $ua->request($req);
is($res->code, 201, "POST test domain");
$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed test domain");
my $domain = JSON::from_json($res->decoded_content);


my $customer_map = {};
my $subscriber_map = {};
my $package_map = {};
my $voucher_map = {};
my $profile_map = {};

my $request_count = 0;

#goto SKIP;
{
    my $profile = _create_billing_profile('PROFILE_1');

    my $customer = _create_customer(billing_profile_definition => 'id',
        billing_profile_id => $profile->{id},);
    my $subscriber = _create_subscriber($customer);
    my $voucher_1 = _create_voucher(10,'test1'.$t,$customer);
    my $voucher_2 = _create_voucher(10,'test2'.$t,$customer,undef,valid_until => '2010-01-01 00:00:00');

    my $request_token = $t."_".$request_count; $request_count++;
    _perform_topup_cash({ id => 'invalid' },0.5,undef,$request_token,422);
    _check_topup_log('failing topup cash validation (subscriber_id): ',[
        { outcome => 'failed', request_token => $request_token, message => 'Validation failed. field=\'subscriber_id\'' }
    ],'request_token='.$request_token);

    $request_token = $t."_".$request_count; $request_count++;
    _perform_topup_cash($subscriber,'invalid_amount',undef,$request_token,422);
    _check_topup_log('failing topup cash validation (amount): ',[
        { outcome => 'failed', request_token => $request_token, message => 'Value cannot be converted to money' }
    ],'request_token='.$request_token);

    $request_token = $t."_".$request_count; $request_count++;
    _perform_topup_cash($subscriber,50,{ id => 'invalid' },$request_token,422);
    _check_topup_log('failing topup cash validation (package_id): ',[
        { outcome => 'failed', request_token => $request_token, message => 'Validation failed. field=\'package_id\'' }
    ],'request_token='.$request_token);

    $request_token = $t."_".$request_count; $request_count++;
    _perform_topup_voucher($subscriber,{ code => 'invalid' },$request_token,422);
    _check_topup_log('failing topup voucher validation (voucher code): ',[
        { outcome => 'failed', request_token => $request_token, message => 'Invalid voucher code \'invalid\'' }
    ],'request_token='.$request_token);

    $request_token = $t."_".$request_count; $request_count++;
    $request_token .= 'a' x (256 - length($request_token));
    _perform_topup_voucher($subscriber,$voucher_1,$request_token,422);
    _check_topup_log('failing topup voucher validation (request_token): ',[
        { outcome => 'failed', request_token => substr($request_token,0,255), message => 'Validation failed. field=\'request_token\'' }, #'Field should not exceed 255 characters' }
    ],'request_token='.substr($request_token,0,255));

    $request_token = $t."_".$request_count; $request_count++;
    _perform_topup_voucher($subscriber,$voucher_1,$request_token);
    $request_token = $t."_".$request_count; $request_count++;
    _perform_topup_voucher($subscriber,$voucher_1,$request_token,422);
    _check_topup_log('failing topup voucher validation (voucher used): ',[
        { outcome => 'failed', request_token => $request_token, message => 'already used' }
    ],'request_token='.$request_token);

    $request_token = $t."_".$request_count; $request_count++;
    _perform_topup_voucher($subscriber,$voucher_2,$request_token,422);
    _check_topup_log('failing topup voucher validation (voucher expired): ',[
        { outcome => 'failed', request_token => $request_token, message => 'expired' }
    ],'request_token='.$request_token);

}

#SKIP:
{
    my $profile_initial_1 = _create_billing_profile('INITIAL1');
    my $profile_topup_1 = _create_billing_profile('TOPUP1');
    #my $profile_underrun = _create_billing_profile('UNDERRUN');

    my $package_1 = _create_profile_package('1st','month',1, initial_balance => 100,
            #carry_over_mode => 'discard', underrun_lock_threshold => 50, underrun_lock_level => 4, underrun_profile_threshold => 50,
            initial_profiles => [ { profile_id => $profile_initial_1->{id}, }, ],
            topup_profiles => [ { profile_id => $profile_topup_1->{id}, }, ],
            #underrun_profiles => [ { profile_id => $profile_underrun->{id}, }, ],
            );

    my $customer = _create_customer(billing_profile_definition => 'package',
        profile_package_id => $package_1->{id},);
    my $subscriber = _create_subscriber($customer);
    my $voucher_1 = _create_voucher(10,'test3'.$t,$customer);

    my $request_token_1 = $t."_".$request_count; $request_count++;
    _perform_topup_cash($subscriber,0.5,undef,$request_token_1);
    my $request_token_2 = $t."_".$request_count; $request_count++;
    _perform_topup_voucher($subscriber,$voucher_1,$request_token_2);

    _check_topup_log('successful topups - subscriber_id, outcome filter: ',[
        { outcome => 'ok', request_token => $request_token_1 },
        { outcome => 'ok', request_token => $request_token_2 },
    ],'subscriber_id='.$subscriber->{id}.'&outcome=ok');

    _check_topup_log('successful topups - contract_id filter: ',[
        { outcome => 'ok', request_token => $request_token_1 },
        { outcome => 'ok', request_token => $request_token_2 },
    ],'contract_id='.$customer->{id});

    my $profile_initial_2 = _create_billing_profile('INITIAL2');
    my $profile_topup_2 = _create_billing_profile('TOPUP2');
    #my $profile_underrun = _create_billing_profile('UNDERRUN');

    my $package_2 = _create_profile_package('1st','month',1, initial_balance => 100,
            #carry_over_mode => 'discard', underrun_lock_threshold => 50, underrun_lock_level => 4, underrun_profile_threshold => 50,
            initial_profiles => [ { profile_id => $profile_initial_2->{id}, }, ],
            topup_profiles => [ { profile_id => $profile_topup_2->{id}, }, ],
            #underrun_profiles => [ { profile_id => $profile_underrun->{id}, }, ],
            );

    my $voucher_2 = _create_voucher(30,'test4'.$t,$customer,$package_2);
    my $request_token_3 = $t."_".$request_count; $request_count++;
    _perform_topup_voucher($subscriber,$voucher_2,$request_token_3);

    _check_topup_log('successful topups - voucher_id filter: ',[
        { outcome => 'ok', request_token => $request_token_3 },
    ],'voucher_id='.$voucher_2->{id});

    _check_topup_log('successful topups - amount_above filter: ',[
        { outcome => 'ok', request_token => $request_token_2 },
        { outcome => 'ok', request_token => $request_token_3 },
    ],'amount_above=1&subscriber_id='.$subscriber->{id});

    _check_topup_log('successful topups - amount_below filter: ',[
        { outcome => 'ok', request_token => $request_token_1 },
        { outcome => 'ok', request_token => $request_token_2 },
    ],'amount_below=10&subscriber_id='.$subscriber->{id});

    _check_topup_log('successful topups - timestamp_from filter: ',[
        { outcome => 'ok', request_token => $request_token_1 },
        { outcome => 'ok', request_token => $request_token_2 },
        { outcome => 'ok', request_token => $request_token_3 },
    ],'timestamp_from=2000-01-01T00:00:00&subscriber_id='.$subscriber->{id});

    _check_topup_log('successful topups - balance before/after: ',[
        { outcome => 'ok', cash_balance_before => 1, cash_balance_after => 1.5, request_token => $request_token_1 },
        { outcome => 'ok', cash_balance_before => 1.5, cash_balance_after => 11.5, request_token => $request_token_2 },
        { outcome => 'ok', cash_balance_before => 11.5, cash_balance_after => 41.5, request_token => $request_token_3 },
    ],'contract_id='.$customer->{id});

    _check_topup_log('successful topups - package before/after: ',[
        { outcome => 'ok', package_before_id=> $package_1->{id}, package_after_id=> $package_1->{id}, request_token => $request_token_1 },
        { outcome => 'ok', package_before_id=> $package_1->{id}, package_after_id=> $package_1->{id}, request_token => $request_token_2 },
        { outcome => 'ok', package_before_id=> $package_1->{id}, package_after_id=> $package_2->{id}, request_token => $request_token_3 },
    ],'contract_id='.$customer->{id});

    _check_topup_log('successful topups - profile before/after: ',[
        { outcome => 'ok', profile_before_id => $profile_initial_1->{id}, profile_after_id => $profile_topup_1->{id}, request_token => $request_token_1 },
        { outcome => 'ok', profile_before_id => $profile_topup_1->{id}, profile_after_id => $profile_topup_1->{id}, request_token => $request_token_2 },
        { outcome => 'ok', profile_before_id => $profile_topup_1->{id}, profile_after_id => $profile_topup_2->{id},request_token => $request_token_3 },
    ],'contract_id='.$customer->{id});

}

SKIP:
{
    my $profile = _create_billing_profile('PROFILE_2');

    my $customer = _create_customer(billing_profile_definition => 'id',
        billing_profile_id => $profile->{id},);

    my $cash_balance = 5; # euro
    _perform_set_balance($customer,$cash_balance,0);

    _check_topup_log('set cash balance: ',[
        {
            outcome => 'ok',
            amount => $cash_balance,
            profile_before_id => $profile->{id},
            profile_after_id => $profile->{id},
            cash_balance_before => 0,
            cash_balance_after => $cash_balance,
        },
    ],'contract_id='.$customer->{id});

    # patch free time balance only:
    $req = HTTP::Request->new('PATCH', $uri.'/api/customerbalances/'.$customer->{id});
    $req->header('Prefer' => 'return=representation');
    $req->header('Content-Type' => 'application/json-patch+json');
    $req->content(JSON::to_json(
        [ { op => 'replace', path => '/free_time_balance', value => 0 } ]
    , { allow_nonref => 1, allow_blessed => 1, convert_blessed => 1, pretty => 0 }));
    $res = $ua->request($req);
    is($res->code, 200, "patch customer balances free time balance only");
    my $customerbalance = JSON::from_json($res->decoded_content, { allow_nonref => 1, });

    _check_topup_log('patch free time balance: ',[
        {
            outcome => 'ok',
            amount => $cash_balance,
            profile_before_id => $profile->{id},
            profile_after_id => $profile->{id},
            cash_balance_before => 0,
            cash_balance_after => $cash_balance,
        },
        {
            outcome => 'ok',
            amount => 0,
            profile_before_id => $profile->{id},
            profile_after_id => $profile->{id},
            cash_balance_before => $cash_balance,
            cash_balance_after => $cash_balance,
        },
    ],'contract_id='.$customer->{id});

}

done_testing;

sub _check_topup_log {

    my ($label,$expected_topup_log,$filter_query) = @_;
    my $total_count = (scalar @$expected_topup_log);
    my $i = 0;
    my $nexturi = $uri.'/api/topuplogs/?page=1&rows=10&order_by_direction=asc&order_by=timestamp'.(defined $filter_query ? '&'.$filter_query : '');
    do {
        $req = HTTP::Request->new('GET',$nexturi);
        $res = $ua->request($req);
        #$res = $ua->get($nexturi);
        is($res->code, 200, $label."fetch topup log collection page");
        my $collection = JSON::from_json($res->decoded_content);
        my $selfuri = $uri . $collection->{_links}->{self}->{href};
        #is($selfuri, $nexturi, $label."check _links.self.href of collection");
        my $colluri = URI->new($selfuri);

        ok($collection->{total_count} == $total_count, $label."check 'total_count' of collection");

        my %q = $colluri->query_form;
        ok(exists $q{page} && exists $q{rows}, $label."check existence of 'page' and 'row' in 'self'");
        my $page = int($q{page});
        my $rows = int($q{rows});
        if($page == 1) {
            ok(!exists $collection->{_links}->{prev}->{href}, $label."check absence of 'prev' on first page");
        } else {
            ok(exists $collection->{_links}->{prev}->{href}, $label."check existence of 'prev'");
        }
        if(($collection->{total_count} / $rows) <= $page) {
            ok(!exists $collection->{_links}->{next}->{href}, $label."check absence of 'next' on last page");
        } else {
            ok(exists $collection->{_links}->{next}->{href}, $label."check existence of 'next'");
        }

        if($collection->{_links}->{next}->{href}) {
            $nexturi = $uri . $collection->{_links}->{next}->{href};
        } else {
            $nexturi = undef;
        }

        # TODO: I'd expect that to be an array ref in any case!
        ok(ref $collection->{_embedded}->{'ngcp:topuplogs'} eq "ARRAY", $label."check if 'ngcp:topuplogs' is array");

        #my $page_items = {};

        foreach my $log_record (@{ $collection->{_embedded}->{'ngcp:topuplogs'} }) {
            #$req = HTTP::Request->new('GET',$uri.$log_record->{_links}->{self}->{href});
            #$res = $ua->request($req);
            #is($res->code, 200, $label."fetch topup log entry");
            #my $got = JSON::from_json($res->decoded_content);
            #is_deeply($got,$log_record,$label.'check topup log entry deeply');
            _compare_log_record($label,$log_record,$expected_topup_log->[$i]);
            $i++
        }

    } while($nexturi);

    ok($i == $total_count,$label."check if all expected items are listed");

}

sub _compare_log_record {
    my ($label,$got,$expected) = @_;

    foreach my $field (keys %$expected) {
        if ('message' eq $field) {
            ok($got->{$field} =~ /$expected->{$field}/,$label."check log '" . $field . "': " . $got->{$field} . " =~ /" . $expected->{$field} . '/');
        } else {
            is($got->{$field},$expected->{$field},$label."check log '" . $field . "': " . $got->{$field} . " = " . $expected->{$field});
        }
    }

}

sub _create_customer {

    my (@further_opts) = @_;
    $req = HTTP::Request->new('POST', $uri.'/api/customers/');
    $req->header('Content-Type' => 'application/json');
    my $req_data = {
        status => "active",
        contact_id => $custcontact->{id},
        type => "sipaccount",
        max_subscribers => undef,
        external_id => undef,
        @further_opts
    };
    $req->content(JSON::to_json($req_data));
    $res = $ua->request($req);
    is($res->code, 201, "create test customer");
    my $request = $req;
    $req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
    $res = $ua->request($req);
    is($res->code, 200, "fetch test customer");
    my $customer = JSON::from_json($res->decoded_content);
    $customer_map->{$customer->{id}} = $customer;
    return $customer;

}

sub _create_profile_package {

    my ($start_mode,$interval_unit,$interval_value,@further_opts) = @_; #$notopup_discard_intervals
    $req = HTTP::Request->new('POST', $uri.'/api/profilepackages/');
    $req->header('Content-Type' => 'application/json');
    $req->header('Prefer' => 'return=representation');
    my $name = $start_mode . ($interval_unit ? '/' . $interval_value . ' ' . $interval_unit : '');
    $req->content(JSON::to_json({
        name => "test '" . $name . "' profile package " . (scalar keys %$package_map) . '_' . $t,
        #description  => "test prof package descr " . (scalar keys %$package_map) . '_' . $t,
        description  => $start_mode . "/" . $interval_value . " " . $interval_unit . "s",
        reseller_id => $default_reseller_id,
        #initial_profiles => [{ profile_id => $billingprofile->{id}, }, ],
        balance_interval_start_mode => $start_mode,
        ($interval_unit ? (balance_interval_value => $interval_value,
                            balance_interval_unit => $interval_unit,) : ()),
        #notopup_discard_intervals => $notopup_discard_intervals,
        @further_opts,
    }));
    $res = $ua->request($req);
    is($res->code, 201, "POST test profilepackage - '" . $name . "'");
    my $profilepackage_uri = $uri.'/'.$res->header('Location');
    $req = HTTP::Request->new('GET', $profilepackage_uri);
    $res = $ua->request($req);
    is($res->code, 200, "fetch POSTed profilepackage - '" . $name . "'");
    my $package = JSON::from_json($res->decoded_content);
    $package_map->{$package->{id}} = $package;
    return $package;

}

sub _create_voucher {

    my ($amount,$code,$customer,$package,@further_opts) = @_;
    my $dtf = DateTime::Format::Strptime->new(
            pattern => '%F %T',
        );
    $req = HTTP::Request->new('POST', $uri.'/api/vouchers/');
    $req->header('Content-Type' => 'application/json');
    my $req_data = {
        amount => $amount * 100.0,
        code => $code,
        customer_id => ($customer ? $customer->{id} : undef),
        package_id => ($package ? $package->{id} : undef),
        reseller_id => $default_reseller_id,
        valid_until => '2037-01-01 00:00:00',
        @further_opts,
        #valid_until => $dtf->format_datetime($valid_until_dt ? $valid_until_dt : NGCP::Panel::Utils::DateTime::current_local->add(years => 1)),
    };
    $req->content(JSON::to_json($req_data));
    $res = $ua->request($req);
    my $label = 'test voucher (' . ($customer ? 'for customer ' . $customer->{id} : 'no customer') . ', ' . ($package ? 'for package ' . $package->{id} : 'no package') . ')';
    is($res->code, 201, "create " . $label);
    my $request = $req;
    $req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
    $res = $ua->request($req);
    is($res->code, 200, "fetch " . $label);
    my $voucher = JSON::from_json($res->decoded_content);
    $voucher_map->{$voucher->{id}} = $voucher;
    return $voucher;

}

sub _create_subscriber {
    my ($customer) = @_;
    $req = HTTP::Request->new('POST', $uri.'/api/subscribers/');
    $req->header('Content-Type' => 'application/json');
    my $req_data = {
        domain_id => $domain->{id},
        username => 'cust_subscriber_' . (scalar keys %$subscriber_map) . '_'.$t,
        password => 'cust_subscriber_password',
        customer_id => $customer->{id},
        #status => "active",
    };
    $req->content(JSON::to_json($req_data));
    $res = $ua->request($req);
    is($res->code, 201, "POST test subscriber");
    my $request = $req;
    $req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
    $res = $ua->request($req);
    is($res->code, 200, "fetch POSTed test subscriber");
    my $subscriber = JSON::from_json($res->decoded_content);
    $subscriber_map->{$subscriber->{id}} = $subscriber;
    return $subscriber;

}

sub _perform_topup_voucher {

    my ($subscriber,$voucher,$request_token,$error_code) = @_;
    $req = HTTP::Request->new('POST', $uri.'/api/topupvouchers/');
    $req->header('Content-Type' => 'application/json');
    my $req_data = {
        code => $voucher->{code},
        subscriber_id => $subscriber->{id},
        (defined $request_token ? (request_token => $request_token) : ()),
    };
    $req->content(JSON::to_json($req_data));
    $res = $ua->request($req);
    $error_code //= 204;
    is($res->code, $error_code, ($error_code == 204 ? 'perform' : 'attempt')." perform topup with voucher " . $voucher->{code});

}

sub _perform_topup_cash {

    my ($subscriber,$amount,$package,$request_token,$error_code) = @_;
    $req = HTTP::Request->new('POST', $uri.'/api/topupcash/');
    $req->header('Content-Type' => 'application/json');
    my $req_data = {
        amount => ( looks_like_number($amount) ? $amount * 100.0 : $amount),
        package_id => ($package ? $package->{id} : undef),
        subscriber_id => $subscriber->{id},
        (defined $request_token ? (request_token => $request_token) : ()),
    };
    $req->content(JSON::to_json($req_data));
    $res = $ua->request($req);
    $error_code //= 204;
    is($res->code, $error_code, ($error_code == 204 ? 'perform' : 'attempt')." topup with amount " . ( looks_like_number($amount) ? $amount * 100.0 . ' cents' : $amount) . ", " . ($package ? 'package id ' . $package->{id} : 'no package'));

}

sub _perform_set_balance {

    my ($customer,$cash_balance,$free_time_balance,$error_code) = @_;
    $req = HTTP::Request->new('PUT', $uri.'/api/customerbalances/' . $customer->{id});
    $req->header('Content-Type' => 'application/json');
    my $req_data = {
        cash_balance => $cash_balance,
        free_time_balance => $free_time_balance,
    };
    $req->content(JSON::to_json($req_data));
    $res = $ua->request($req);
    $error_code //= 204;
    is($res->code, $error_code, ($error_code == 204 ? 'perform' : 'attempt')." setting cash_balance to " . $cash_balance . ", free_time_balance to $free_time_balance");

}

sub _create_billing_profile {
    my ($name) = @_;
    $req = HTTP::Request->new('POST', $uri.'/api/billingprofiles/');
    $req->header('Content-Type' => 'application/json');
    $req->header('Prefer' => 'return=representation');
    my $req_data = {
        name => $name." $t",
        handle  => $name."_$t",
        reseller_id => $default_reseller_id,
    };
    $req->content(JSON::to_json($req_data));
    $res = $ua->request($req);
    is($res->code, 201, "POST test billing profile " . $name);
    my $request = $req;
    $req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
    $res = $ua->request($req);
    is($res->code, 200, "fetch POSTed billing profile " . $name);
    my $billingprofile = JSON::from_json($res->decoded_content);
    $profile_map->{$billingprofile->{id}} = $billingprofile;
    return $billingprofile;
}
