/*
 * Copyright 2010-2013 Ning, Inc.
 * 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.subscription.api.user;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import javax.annotation.Nullable;
import javax.inject.Inject;

import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.Period;
import org.killbill.billing.ObjectType;
import org.killbill.billing.api.TestApiListener;
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Duration;
import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.TimeUnit;
import org.killbill.billing.dao.MockNonEntityDao;
import org.killbill.billing.entitlement.api.EntitlementSpecifier;
import org.killbill.billing.entitlement.api.SubscriptionEventType;
import org.killbill.billing.invoice.api.DryRunArguments;
import org.killbill.billing.invoice.api.DryRunType;
import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
import org.killbill.billing.subscription.api.SubscriptionBaseWithAddOns;
import org.killbill.billing.subscription.api.SubscriptionBaseWithAddOnsSpecifier;
import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
import org.killbill.billing.subscription.events.phase.PhaseEvent;
import org.killbill.billing.subscription.events.user.ApiEvent;
import org.killbill.billing.subscription.events.user.ApiEventType;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.clock.Clock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ImmutableList;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;

public class TestSubscriptionHelper {

    private final Logger log = LoggerFactory.getLogger(TestSubscriptionHelper.class);

    private final SubscriptionBaseInternalApi subscriptionApi;
    private final Clock clock;
    private final MockNonEntityDao mockNonEntityDao;
    private final InternalCallContext internalCallContext;
    private final TestApiListener testListener;
    private final SubscriptionDao dao;
    private final InternalCallContextFactory internalCallContextFactory;

    @Inject
    public TestSubscriptionHelper(final SubscriptionBaseInternalApi subscriptionApi,
                                  final Clock clock,
                                  final MockNonEntityDao mockNonEntityDao,
                                  final InternalCallContext internalCallContext,
                                  final TestApiListener testListener,
                                  final SubscriptionDao dao,
                                  final InternalCallContextFactory internalCallContextFactory) {
        this.subscriptionApi = subscriptionApi;
        this.clock = clock;
        this.mockNonEntityDao = mockNonEntityDao;
        this.internalCallContext = internalCallContext;
        this.testListener = testListener;
        this.dao = dao;
        this.internalCallContextFactory = internalCallContextFactory;
    }

    public DryRunArguments createDryRunArguments(final UUID subscriptionId, final UUID bundleId, final PlanPhaseSpecifier spec, final LocalDate requestedDate, final SubscriptionEventType type, final BillingActionPolicy billingActionPolicy) {
        return new DryRunArguments() {
            @Override
            public DryRunType getDryRunType() {
                return DryRunType.SUBSCRIPTION_ACTION;
            }

            @Override
            public PlanPhaseSpecifier getPlanPhaseSpecifier() {
                return spec;
            }

            @Override
            public SubscriptionEventType getAction() {
                return type;
            }

            @Override
            public UUID getSubscriptionId() {
                return subscriptionId;
            }

            @Override
            public LocalDate getEffectiveDate() {
                return requestedDate;
            }

            @Override
            public UUID getBundleId() {
                return bundleId;
            }

            @Override
            public BillingActionPolicy getBillingActionPolicy() {
                return billingActionPolicy;
            }

            @Override
            public List<PlanPhasePriceOverride> getPlanPhasePriceOverrides() {
                return null;
            }
        };
    }

    public DefaultSubscriptionBase createSubscription(final SubscriptionBaseBundle bundle, final String productName, final BillingPeriod term, final String planSet, final DateTime requestedDate)
            throws SubscriptionBaseApiException {
        return createSubscription(bundle, productName, term, planSet, null, requestedDate);
    }

    public DefaultSubscriptionBase createSubscription(final SubscriptionBaseBundle bundle, final String productName, final BillingPeriod term, final String planSet, final PhaseType phaseType, final DateTime requestedDate)
            throws SubscriptionBaseApiException {
        return createSubscription(bundle, null, productName, term, planSet, phaseType, requestedDate);
    }

    public DefaultSubscriptionBase createSubscription(final SubscriptionBaseBundle bundle, final SubscriptionBase baseSubscription, final String aoProduct, final BillingPeriod aoTerm, final String aoPriceList) throws SubscriptionBaseApiException {
        return createSubscription(bundle, baseSubscription, aoProduct, aoTerm, aoPriceList, null, null);
    }

    public DefaultSubscriptionBase createSubscription(final SubscriptionBaseBundle bundle, final String productName, final BillingPeriod term, final String planSet)
            throws SubscriptionBaseApiException {
        return createSubscription(bundle, null, productName, term, planSet, null, null);
    }

    public DefaultSubscriptionBase createSubscription(final SubscriptionBaseBundle bundle, final SubscriptionBase baseSubscription, final String productName, final BillingPeriod term, final String planSet, final DateTime requestedDate) throws SubscriptionBaseApiException {
        return createSubscription(bundle, baseSubscription, productName, term, planSet, null, requestedDate);
    }

    public DefaultSubscriptionBase createSubscription(final boolean noEvents, final SubscriptionBaseBundle bundle, final String productName, final BillingPeriod term, final String planSet) throws SubscriptionBaseApiException {
        return createSubscription(noEvents, bundle, null, productName, term, planSet, null, null);
    }

    public DefaultSubscriptionBase createSubscription(final SubscriptionBaseBundle bundle, final SubscriptionBase baseSubscription, final String productName, final BillingPeriod term, final String planSet, final PhaseType phaseType, final DateTime requestedDate)
            throws SubscriptionBaseApiException {
        return createSubscription(false, bundle, baseSubscription, productName, term, planSet, phaseType, requestedDate);
    }

    public DefaultSubscriptionBase createSubscription(final boolean noEvents, @Nullable final SubscriptionBaseBundle bundle, final SubscriptionBase baseSubscription, final String productName, final BillingPeriod term, final String planSet, final PhaseType phaseType, final DateTime requestedDate)
    throws SubscriptionBaseApiException {
        // Make sure the right account information is used
        final InternalCallContext internalCallContext = bundle == null ? this.internalCallContext : internalCallContextFactory.createInternalCallContext(bundle.getAccountId(),
                                                                                                                                                         ObjectType.ACCOUNT,
                                                                                                                                                         this.internalCallContext.getUpdatedBy(),
                                                                                                                                                         this.internalCallContext.getCallOrigin(),
                                                                                                                                                         this.internalCallContext.getContextUserType(),
                                                                                                                                                         this.internalCallContext.getUserToken(),
                                                                                                                                                         this.internalCallContext.getTenantRecordId());

        boolean bundleExists = false;
        if (bundle != null) {
            try {
                bundleExists = (subscriptionApi.getBundleFromId(bundle.getId(), internalCallContext) != null);
            } catch (final SubscriptionBaseApiException ignored) {
            }
        }

        if (!noEvents && (requestedDate == null || requestedDate.compareTo(clock.getUTCNow()) <= 0)) {
            testListener.pushExpectedEvent(NextEvent.CREATE);
        }

        final ImmutableList<EntitlementSpecifier> entitlementSpecifiers = ImmutableList.<EntitlementSpecifier>of(new EntitlementSpecifier() {
            @Override
            public PlanPhaseSpecifier getPlanPhaseSpecifier() {
                return new PlanPhaseSpecifier(productName, term, planSet, phaseType);
            }

            @Override
            public List<PlanPhasePriceOverride> getOverrides() {
                return null;
            }
        });
        final SubscriptionBaseWithAddOnsSpecifier subscriptionBaseWithAddOnsSpecifier = new SubscriptionBaseWithAddOnsSpecifier(bundle == null ||!bundleExists ? null : bundle.getId(),
                                                                                                                                bundle == null ? null : bundle.getExternalKey(),
                                                                                                                                entitlementSpecifiers,
                                                                                                                                requestedDate,
                                                                                                                                false);
        final SubscriptionBaseWithAddOns subscriptionBaseWithAddOns = subscriptionApi.createBaseSubscriptionsWithAddOns(ImmutableList.<SubscriptionBaseWithAddOnsSpecifier>of(subscriptionBaseWithAddOnsSpecifier),
                                                                                                                        false,
                                                                                                                        internalCallContext).get(0);
        final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) subscriptionBaseWithAddOns.getSubscriptionBaseList().get(0);
        assertNotNull(subscription);

        testListener.assertListenerStatus();

        mockNonEntityDao.addTenantRecordIdMapping(subscription.getId(), internalCallContext);
        mockNonEntityDao.addAccountRecordIdMapping(subscription.getId(), internalCallContext);

        mockNonEntityDao.addTenantRecordIdMapping(subscription.getBundleId(), internalCallContext);
        mockNonEntityDao.addAccountRecordIdMapping(subscription.getBundleId(), internalCallContext);

        return subscription;
    }

    public void checkNextPhaseChange(final DefaultSubscriptionBase subscription, final int expPendingEvents, final DateTime expPhaseChange) {
        final List<SubscriptionBaseEvent> events = dao.getPendingEventsForSubscription(subscription.getId(), internalCallContext);
        assertNotNull(events);
        printEvents(events);
        assertEquals(events.size(), expPendingEvents);
        if (events.size() > 0 && expPhaseChange != null) {
            boolean foundPhase = false;
            boolean foundChange = false;

            for (final SubscriptionBaseEvent cur : events) {
                if (cur instanceof PhaseEvent) {
                    assertEquals(foundPhase, false);
                    foundPhase = true;
                    assertEquals(cur.getEffectiveDate(), expPhaseChange);
                } else if (cur instanceof ApiEvent) {
                    final ApiEvent uEvent = (ApiEvent) cur;
                    assertEquals(ApiEventType.CHANGE, uEvent.getApiEventType());
                    assertEquals(foundChange, false);
                    foundChange = true;
                } else {
                    assertFalse(true);
                }
            }
        }
    }

    public void assertDateWithin(final DateTime in, final DateTime lower, final DateTime upper) {
        assertTrue(in.isEqual(lower) || in.isAfter(lower), "in=" + in + ", lower=" + lower);
        assertTrue(in.isEqual(upper) || in.isBefore(upper), "in=" + in + ", upper=" + upper);
    }

    public Duration getDurationMonth(final int months) {
        final Duration result = new Duration() {
            @Override
            public TimeUnit getUnit() {
                return TimeUnit.MONTHS;
            }

            @Override
            public int getNumber() {
                return months;
            }

            @Override
            public DateTime addToDateTime(final DateTime dateTime) {
                return null;
            }
            @Override
            public LocalDate addToLocalDate(final LocalDate localDate) {
                return null;
            }
            @Override
            public Period toJodaPeriod() {
                throw new UnsupportedOperationException();
            }
        };
        return result;
    }

    public PlanPhaseSpecifier getProductSpecifier(final String productName, final String priceList,
                                                  final BillingPeriod term,
                                                  @Nullable final PhaseType phaseType) {
        return new PlanPhaseSpecifier(productName, term, priceList, phaseType);
    }

    public void printEvents(final List<SubscriptionBaseEvent> events) {
        for (final SubscriptionBaseEvent cur : events) {
            log.debug("Inspect event " + cur);
        }
    }

    public static DateTime addOrRemoveDuration(final DateTime input, final List<Duration> durations, final boolean add) {
        DateTime result = input;
        for (final Duration cur : durations) {
            switch (cur.getUnit()) {
                case DAYS:
                    result = add ? result.plusDays(cur.getNumber()) : result.minusDays(cur.getNumber());
                    break;

                case MONTHS:
                    result = add ? result.plusMonths(cur.getNumber()) : result.minusMonths(cur.getNumber());
                    break;

                case YEARS:
                    result = add ? result.plusYears(cur.getNumber()) : result.minusYears(cur.getNumber());
                    break;
                case UNLIMITED:
                default:
                    throw new RuntimeException("Trying to move to unlimited time period");
            }
        }
        return result;
    }

    public static DateTime addDuration(final DateTime input, final List<Duration> durations) {
        return addOrRemoveDuration(input, durations, true);
    }

    public static DateTime addDuration(final DateTime input, final Duration duration) {
        final List<Duration> list = new ArrayList<Duration>();
        list.add(duration);
        return addOrRemoveDuration(input, list, true);
    }
}
