/*
 * Copyright 2014-2018 Groupon, Inc
 * Copyright 2014-2018 The Billing Project, LLC
 *
 * The Billing Project licenses this file to you under the Apache License, version 2.0
 * (the "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at:
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

package org.killbill.billing.jaxrs;

import java.math.BigDecimal;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import org.killbill.billing.ErrorCode;
import org.killbill.billing.client.KillBillClientException;
import org.killbill.billing.client.RequestOptions;
import org.killbill.billing.client.model.Account;
import org.killbill.billing.client.model.ComboPaymentTransaction;
import org.killbill.billing.client.model.Payment;
import org.killbill.billing.client.model.PaymentMethod;
import org.killbill.billing.client.model.PaymentMethodPluginDetail;
import org.killbill.billing.client.model.PaymentTransaction;
import org.killbill.billing.client.model.PluginProperty;
import org.killbill.billing.client.model.Tenant;
import org.killbill.billing.client.model.TenantKey;
import org.killbill.billing.notification.plugin.api.ExtBusEventType;
import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.tenant.api.TenantKV;
import org.testng.Assert;
import org.testng.annotations.Test;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Resources;
import org.awaitility.Awaitility;
import org.awaitility.Duration;

public class TestTenantKV extends TestJaxrsBase {

    @Test(groups = "slow", description = "Upload and retrieve a per plugin config")
    public void testPerTenantPluginConfig() throws Exception {
        final String pluginName = "PLUGIN_FOO";

        final String pluginPath = Resources.getResource("plugin.yml").getPath();
        callbackServlet.pushExpectedEvent(ExtBusEventType.TENANT_CONFIG_CHANGE);
        final TenantKey tenantKey0 = killBillClient.registerPluginConfigurationForTenant(pluginName, pluginPath, createdBy, reason, comment);
        callbackServlet.assertListenerStatus();
        Assert.assertEquals(tenantKey0.getKey(), TenantKV.TenantKey.PLUGIN_CONFIG_.toString() + pluginName);

        final TenantKey tenantKey1 = killBillClient.getPluginConfigurationForTenant(pluginName);
        Assert.assertEquals(tenantKey1.getKey(), TenantKV.TenantKey.PLUGIN_CONFIG_.toString() + pluginName);
        Assert.assertEquals(tenantKey1.getValues().size(), 1);

        killBillClient.unregisterPluginConfigurationForTenant(pluginName, createdBy, reason, comment);
        final TenantKey tenantKey2 = killBillClient.getPluginConfigurationForTenant(pluginName);
        Assert.assertEquals(tenantKey2.getKey(), TenantKV.TenantKey.PLUGIN_CONFIG_.toString() + pluginName);
        Assert.assertEquals(tenantKey2.getValues().size(), 0);
    }

    @Test(groups = "slow", description = "Upload and retrieve a per plugin payment state machine config")
    public void testPerTenantPluginPaymentStateMachineConfig() throws Exception {
        final RequestOptions requestOptionsForOriginalTenant = requestOptions;

        // Create another tenant - it will have a different state machine
        final Tenant otherTenantWithDifferentStateMachine = createTenant(UUID.randomUUID().toString(), UUID.randomUUID().toString(), true);

        // Verify initial state
        final TenantKey emptyTenantKey = killBillClient.getPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, requestOptionsForOriginalTenant);
        Assert.assertEquals(emptyTenantKey.getValues().size(), 0);
        final TenantKey emptyTenantKeyOtherTenant = killBillClient.getPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, requestOptions);
        Assert.assertEquals(emptyTenantKeyOtherTenant.getValues().size(), 0);

        callbackServlet.pushExpectedEvent(ExtBusEventType.TENANT_CONFIG_CHANGE);
        final String stateMachineConfigPath = Resources.getResource("SimplePaymentStates.xml").getPath();
        final TenantKey tenantKey0 = killBillClient.registerPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, stateMachineConfigPath, requestOptions);
        callbackServlet.assertListenerStatus();
        Assert.assertEquals(tenantKey0.getKey(), TenantKV.TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_.toString() + PLUGIN_NAME);

        // Verify only the other tenant has the new state machine
        final TenantKey emptyTenantKey1 = killBillClient.getPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, requestOptionsForOriginalTenant);
        Assert.assertEquals(emptyTenantKey1.getValues().size(), 0);
        final TenantKey tenantKey1OtherTenant = killBillClient.getPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, requestOptions);
        Assert.assertEquals(tenantKey1OtherTenant.getKey(), TenantKV.TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_.toString() + PLUGIN_NAME);
        Assert.assertEquals(tenantKey1OtherTenant.getValues().size(), 1);

        // Create an auth in both tenant
        final Payment payment = createComboPaymentTransaction(requestOptionsForOriginalTenant);
        final Payment paymentOtherTenant = createComboPaymentTransaction(requestOptions);

        // Void in the first tenant (allowed by the default state machine)
        callbackServlet.pushExpectedEvent(ExtBusEventType.PAYMENT_SUCCESS);
        final Payment voidPayment = killBillClient.voidPayment(payment.getPaymentId(), payment.getPaymentExternalKey(), UUID.randomUUID().toString(), ImmutableList.<String>of(), ImmutableMap.<String, String>of(), requestOptionsForOriginalTenant);
        callbackServlet.assertListenerStatus();
        Assert.assertEquals(voidPayment.getTransactions().get(0).getStatus(), TransactionStatus.SUCCESS.toString());
        Assert.assertEquals(voidPayment.getTransactions().get(1).getStatus(), TransactionStatus.SUCCESS.toString());

        // Void in the other tenant (disallowed)
        try {
            killBillClient.voidPayment(paymentOtherTenant.getPaymentId(), paymentOtherTenant.getPaymentExternalKey(), UUID.randomUUID().toString(), ImmutableList.<String>of(), ImmutableMap.<String, String>of(), requestOptions);
            Assert.fail();
        } catch (final KillBillClientException e) {
            Assert.assertEquals((int) e.getBillingException().getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
        }
        callbackServlet.assertListenerStatus();

        // Remove the custom state machine
        callbackServlet.pushExpectedEvent(ExtBusEventType.TENANT_CONFIG_DELETION);
        killBillClient.unregisterPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, requestOptions);
        callbackServlet.assertListenerStatus();
        final TenantKey tenantKey2 = killBillClient.getPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, requestOptions);
        Assert.assertEquals(tenantKey2.getKey(), TenantKV.TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_.toString() + PLUGIN_NAME);
        Assert.assertEquals(tenantKey2.getValues().size(), 0);

        final AtomicReference<Payment> voidPaymentOtherTenant2Ref = new AtomicReference<Payment>();
        Awaitility.await()
                  .atMost(8, TimeUnit.SECONDS)
                  .pollInterval(Duration.TWO_SECONDS)
                  .until(new Callable<Boolean>() {
                      @Override
                      public Boolean call() throws Exception {
                          // The void should now go through
                          try {
                              callbackServlet.pushExpectedEvent(ExtBusEventType.PAYMENT_SUCCESS);
                              final Payment voidPaymentOtherTenant2 = killBillClient.voidPayment(paymentOtherTenant.getPaymentId(), paymentOtherTenant.getPaymentExternalKey(), UUID.randomUUID().toString(), ImmutableList.<String>of(), ImmutableMap.<String, String>of(), requestOptions);
                              callbackServlet.assertListenerStatus();
                              voidPaymentOtherTenant2Ref.set(voidPaymentOtherTenant2);
                              return voidPaymentOtherTenant2 != null;
                          } catch (final KillBillClientException e) {
                              // Invalidation hasn't happened yet
                              return false;
                          }
                      }
                  });
        Assert.assertEquals(voidPaymentOtherTenant2Ref.get().getTransactions().get(0).getStatus(), TransactionStatus.SUCCESS.toString());
        Assert.assertEquals(voidPaymentOtherTenant2Ref.get().getTransactions().get(1).getStatus(), TransactionStatus.SUCCESS.toString());
    }

    private Payment createComboPaymentTransaction(final RequestOptions requestOptions) throws KillBillClientException {
        final Account accountJson = getAccount();
        accountJson.setAccountId(null);

        final PaymentMethodPluginDetail info = new PaymentMethodPluginDetail();
        info.setProperties(null);

        final String paymentMethodExternalKey = UUID.randomUUID().toString();
        final PaymentMethod paymentMethodJson = new PaymentMethod(null, paymentMethodExternalKey, null, true, PLUGIN_NAME, info);

        final String authTransactionExternalKey = UUID.randomUUID().toString();
        final PaymentTransaction authTransactionJson = new PaymentTransaction();
        authTransactionJson.setAmount(BigDecimal.TEN);
        authTransactionJson.setCurrency(accountJson.getCurrency());
        authTransactionJson.setPaymentExternalKey(UUID.randomUUID().toString());
        authTransactionJson.setTransactionExternalKey(authTransactionExternalKey);
        authTransactionJson.setTransactionType("AUTHORIZE");

        callbackServlet.pushExpectedEvents(ExtBusEventType.ACCOUNT_CREATION, ExtBusEventType.ACCOUNT_CHANGE, ExtBusEventType.PAYMENT_SUCCESS);
        final ComboPaymentTransaction comboAuthorization = new ComboPaymentTransaction(accountJson, paymentMethodJson, authTransactionJson, ImmutableList.<PluginProperty>of(), ImmutableList.<PluginProperty>of());
        final Payment payment = killBillClient.createPayment(comboAuthorization, ImmutableMap.<String, String>of(), requestOptions);
        callbackServlet.assertListenerStatus();
        Assert.assertEquals(payment.getTransactions().get(0).getStatus(), TransactionStatus.SUCCESS.toString());

        return payment;
    }
}
