/*
 * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies
 *
 * Please see distribution for license.
 */
package com.opengamma.strata.product.bond;

import static com.opengamma.strata.basics.currency.Currency.USD;
import static com.opengamma.strata.basics.date.BusinessDayConventions.FOLLOWING;
import static com.opengamma.strata.basics.date.HolidayCalendarIds.SAT_SUN;
import static com.opengamma.strata.collect.TestHelper.assertSerialization;
import static com.opengamma.strata.collect.TestHelper.coverBeanEquals;
import static com.opengamma.strata.collect.TestHelper.coverImmutableBean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;

import java.time.LocalDate;
import java.time.Period;
import java.util.Optional;

import org.junit.jupiter.api.Test;

import com.google.common.collect.ImmutableList;
import com.opengamma.strata.basics.ReferenceData;
import com.opengamma.strata.basics.currency.Currency;
import com.opengamma.strata.basics.date.BusinessDayAdjustment;
import com.opengamma.strata.basics.date.DayCount;
import com.opengamma.strata.basics.date.DayCounts;
import com.opengamma.strata.basics.date.DaysAdjustment;
import com.opengamma.strata.basics.schedule.Frequency;
import com.opengamma.strata.basics.schedule.PeriodicSchedule;
import com.opengamma.strata.basics.schedule.StubConvention;
import com.opengamma.strata.basics.value.Rounding;
import com.opengamma.strata.product.LegalEntityId;
import com.opengamma.strata.product.SecurityId;

/**
 * Test {@link BondFuture}.
 */
@SuppressWarnings("unchecked")
public class BondFutureTest {

  private static final ReferenceData REF_DATA = ReferenceData.standard();

  // Underlying bonds
  private static final LegalEntityId ISSUER_ID = LegalEntityId.of("OG-Ticker", "GOVT1");
  private static final FixedCouponBondYieldConvention YIELD_CONVENTION = FixedCouponBondYieldConvention.US_STREET;
  private static final double NOTIONAL = 100000d;
  private static final DaysAdjustment SETTLEMENT_DAYS = DaysAdjustment.ofBusinessDays(1, SAT_SUN);
  private static final DayCount DAY_COUNT = DayCounts.ACT_ACT_ICMA;
  private static final BusinessDayAdjustment BUSINESS_ADJUST = BusinessDayAdjustment.of(FOLLOWING, SAT_SUN);
  private static final DaysAdjustment EX_COUPON = DaysAdjustment.NONE;
  private static final SecurityId SECURITY_ID = SecurityId.of("OG-Test", "BondFuture");
  private static final SecurityId SECURITY_ID2 = SecurityId.of("OG-Test", "BondFuture2");

  private static final int NB_BOND = 7;
  private static final double[] RATE = new double[] {0.01375, 0.02125, 0.0200, 0.02125, 0.0225, 0.0200, 0.0175};
  private static final LocalDate[] START_DATE = new LocalDate[] {
      LocalDate.of(2010, 11, 30), LocalDate.of(2010, 12, 31), LocalDate.of(2011, 1, 31), LocalDate.of(2008, 2, 29),
      LocalDate.of(2011, 3, 31), LocalDate.of(2011, 4, 30), LocalDate.of(2011, 5, 31)};
  private static final Period[] BOND_TENOR = new Period[] {
      Period.ofYears(5), Period.ofYears(5), Period.ofYears(5), Period.ofYears(8),
      Period.ofYears(5), Period.ofYears(5), Period.ofYears(5)};
  @SuppressWarnings("unchecked")
  private static final FixedCouponBond[] BOND_PRODUCT = new FixedCouponBond[NB_BOND];
  private static final ResolvedFixedCouponBond[] RESOLVED_BASKET = new ResolvedFixedCouponBond[NB_BOND];

  static {
    for (int i = 0; i < NB_BOND; ++i) {
      LocalDate endDate = START_DATE[i].plus(BOND_TENOR[i]);
      PeriodicSchedule periodSchedule = PeriodicSchedule.of(
          START_DATE[i], endDate, Frequency.P6M, BUSINESS_ADJUST, StubConvention.SHORT_INITIAL, false);
      FixedCouponBond product = FixedCouponBond.builder()
          .securityId(SecurityId.of("OG-Test", "Bond " + i))
          .dayCount(DAY_COUNT)
          .fixedRate(RATE[i])
          .legalEntityId(ISSUER_ID)
          .currency(USD)
          .notional(NOTIONAL)
          .accrualSchedule(periodSchedule)
          .settlementDateOffset(SETTLEMENT_DAYS)
          .yieldConvention(YIELD_CONVENTION)
          .exCouponPeriod(EX_COUPON)
          .build();
      BOND_PRODUCT[i] = product;
      RESOLVED_BASKET[i] = product.resolve(REF_DATA);
    }
  }

  // future specification
  private static final Double[] CONVERSION_FACTOR = new Double[] {.8317, .8565, .8493, .8516, .8540, .8417, .8292};
  private static final LocalDate LAST_TRADING_DATE = LocalDate.of(2011, 9, 30);
  private static final LocalDate FIRST_NOTICE_DATE = LocalDate.of(2011, 8, 31);
  private static final LocalDate LAST_NOTICE_DATE = LocalDate.of(2011, 10, 4);
  private static final LocalDate FIRST_DELIVERY_DATE = SETTLEMENT_DAYS.adjust(FIRST_NOTICE_DATE, REF_DATA);
  private static final LocalDate LAST_DELIVERY_DATE = SETTLEMENT_DAYS.adjust(LAST_NOTICE_DATE, REF_DATA);
  private static final Rounding ROUNDING = Rounding.ofDecimalPlaces(3);

  //-------------------------------------------------------------------------
  @Test
  public void test_builder_full() {
    BondFuture test = BondFuture.builder()
        .securityId(SECURITY_ID)
        .deliveryBasket(BOND_PRODUCT)
        .conversionFactors(CONVERSION_FACTOR)
        .firstNoticeDate(FIRST_NOTICE_DATE)
        .firstDeliveryDate(FIRST_DELIVERY_DATE)
        .lastNoticeDate(LAST_NOTICE_DATE)
        .lastDeliveryDate(LAST_DELIVERY_DATE)
        .lastTradeDate(LAST_TRADING_DATE)
        .rounding(ROUNDING)
        .build();
    assertThat(test.getDeliveryBasket()).isEqualTo(ImmutableList.copyOf(BOND_PRODUCT));
    assertThat(test.getConversionFactors()).isEqualTo(ImmutableList.copyOf(CONVERSION_FACTOR));
    assertThat(test.getCurrency()).isEqualTo(USD);
    assertThat(test.getNotional()).isEqualTo(NOTIONAL);
    assertThat(test.getFirstNoticeDate()).isEqualTo(FIRST_NOTICE_DATE);
    assertThat(test.getLastNoticeDate()).isEqualTo(LAST_NOTICE_DATE);
    assertThat(test.getFirstDeliveryDate()).isEqualTo(Optional.of(FIRST_DELIVERY_DATE));
    assertThat(test.getLastDeliveryDate()).isEqualTo(Optional.of(LAST_DELIVERY_DATE));
    assertThat(test.getLastTradeDate()).isEqualTo(LAST_TRADING_DATE);
    assertThat(test.getRounding()).isEqualTo(ROUNDING);
  }

  @Test
  public void test_builder_noDeliveryDate() {
    BondFuture test = BondFuture.builder()
        .securityId(SECURITY_ID)
        .deliveryBasket(BOND_PRODUCT)
        .conversionFactors(CONVERSION_FACTOR)
        .firstNoticeDate(FIRST_NOTICE_DATE)
        .lastNoticeDate(LAST_NOTICE_DATE)
        .lastTradeDate(LAST_TRADING_DATE)
        .rounding(ROUNDING)
        .build();
    assertThat(test.getDeliveryBasket()).isEqualTo(ImmutableList.copyOf(BOND_PRODUCT));
    assertThat(test.getConversionFactors()).isEqualTo(ImmutableList.copyOf(CONVERSION_FACTOR));
    assertThat(test.getCurrency()).isEqualTo(USD);
    assertThat(test.getNotional()).isEqualTo(NOTIONAL);
    assertThat(test.getFirstNoticeDate()).isEqualTo(FIRST_NOTICE_DATE);
    assertThat(test.getLastNoticeDate()).isEqualTo(LAST_NOTICE_DATE);
    assertThat(test.getFirstDeliveryDate()).isEqualTo(Optional.empty());
    assertThat(test.getLastDeliveryDate()).isEqualTo(Optional.empty());
    assertThat(test.getLastTradeDate()).isEqualTo(LAST_TRADING_DATE);
    assertThat(test.getRounding()).isEqualTo(ROUNDING);

  }

  @Test
  public void test_builder_fail() {
    // wrong size
    assertThatIllegalArgumentException()
        .isThrownBy(() -> BondFuture.builder()
            .securityId(SECURITY_ID)
            .deliveryBasket(BOND_PRODUCT[0])
            .conversionFactors(CONVERSION_FACTOR)
            .firstNoticeDate(FIRST_NOTICE_DATE)
            .lastNoticeDate(LAST_NOTICE_DATE)
            .lastTradeDate(LAST_TRADING_DATE)
            .rounding(ROUNDING)
            .build());
    // first notice date missing
    assertThatIllegalArgumentException()
        .isThrownBy(() -> BondFuture.builder()
            .securityId(SECURITY_ID)
            .deliveryBasket(BOND_PRODUCT)
            .conversionFactors(CONVERSION_FACTOR)
            .lastNoticeDate(LAST_NOTICE_DATE)
            .lastTradeDate(LAST_TRADING_DATE)
            .rounding(ROUNDING)
            .build());
    // last notice date missing
    assertThatIllegalArgumentException()
        .isThrownBy(() -> BondFuture.builder()
            .securityId(SECURITY_ID)
            .deliveryBasket(BOND_PRODUCT)
            .conversionFactors(CONVERSION_FACTOR)
            .firstNoticeDate(FIRST_NOTICE_DATE)
            .lastTradeDate(LAST_TRADING_DATE)
            .rounding(ROUNDING)
            .build());
    // basket list empty
    assertThatIllegalArgumentException()
        .isThrownBy(() -> BondFuture.builder()
            .securityId(SECURITY_ID)
            .conversionFactors(CONVERSION_FACTOR)
            .firstNoticeDate(FIRST_NOTICE_DATE)
            .lastNoticeDate(LAST_NOTICE_DATE)
            .lastTradeDate(LAST_TRADING_DATE)
            .rounding(ROUNDING)
            .build());
    // notional mismatch
    FixedCouponBond bond0 = BOND_PRODUCT[0];
    FixedCouponBond bond1 = bond0.toBuilder().notional(100).build();
    FixedCouponBond bond2 = bond0.toBuilder().currency(Currency.CAD).build();
    assertThatIllegalArgumentException()
        .isThrownBy(() -> BondFuture.builder()
            .securityId(SECURITY_ID)
            .deliveryBasket(bond0, bond1)
            .conversionFactors(CONVERSION_FACTOR[0], CONVERSION_FACTOR[1])
            .firstNoticeDate(FIRST_NOTICE_DATE)
            .lastNoticeDate(LAST_NOTICE_DATE)
            .firstDeliveryDate(FIRST_DELIVERY_DATE)
            .lastDeliveryDate(LAST_DELIVERY_DATE)
            .lastTradeDate(LAST_TRADING_DATE)
            .rounding(ROUNDING)
            .build());
    // currency mismatch
    assertThatIllegalArgumentException()
        .isThrownBy(() -> BondFuture.builder()
            .securityId(SECURITY_ID)
            .deliveryBasket(bond0, bond2)
            .conversionFactors(CONVERSION_FACTOR[0], CONVERSION_FACTOR[1])
            .firstNoticeDate(FIRST_NOTICE_DATE)
            .lastNoticeDate(LAST_NOTICE_DATE)
            .firstDeliveryDate(FIRST_DELIVERY_DATE)
            .lastDeliveryDate(LAST_DELIVERY_DATE)
            .lastTradeDate(LAST_TRADING_DATE)
            .rounding(ROUNDING)
            .build());
  }

  //-------------------------------------------------------------------------
  @Test
  public void test_resolve() {
    ResolvedBondFuture expected = ResolvedBondFuture.builder()
        .securityId(SECURITY_ID)
        .deliveryBasket(RESOLVED_BASKET)
        .conversionFactors(CONVERSION_FACTOR)
        .lastTradeDate(LAST_TRADING_DATE)
        .firstNoticeDate(FIRST_NOTICE_DATE)
        .lastNoticeDate(LAST_NOTICE_DATE)
        .firstDeliveryDate(FIRST_DELIVERY_DATE)
        .lastDeliveryDate(LAST_DELIVERY_DATE)
        .rounding(ROUNDING)
        .build();
    assertThat(sut().resolve(REF_DATA)).isEqualTo(expected);
  }

  //-------------------------------------------------------------------------
  @Test
  public void coverage() {
    coverImmutableBean(sut());
    coverBeanEquals(sut(), sut2());
  }

  @Test
  public void serialization() {
    assertSerialization(sut());
  }

  //-------------------------------------------------------------------------
  static BondFuture sut() {
    return BondFuture.builder()
        .securityId(SECURITY_ID)
        .deliveryBasket(BOND_PRODUCT)
        .conversionFactors(ImmutableList.copyOf(CONVERSION_FACTOR))
        .firstNoticeDate(FIRST_NOTICE_DATE)
        .firstDeliveryDate(FIRST_DELIVERY_DATE)
        .lastNoticeDate(LAST_NOTICE_DATE)
        .lastDeliveryDate(LAST_DELIVERY_DATE)
        .lastTradeDate(LAST_TRADING_DATE)
        .rounding(ROUNDING)
        .build();
  }

  static BondFuture sut2() {
    return BondFuture.builder()
        .securityId(SECURITY_ID2)
        .conversionFactors(0.9187)
        .deliveryBasket(BOND_PRODUCT[3])
        .firstNoticeDate(FIRST_NOTICE_DATE.plusDays(7))
        .lastNoticeDate(LAST_NOTICE_DATE.plusDays(7))
        .lastTradeDate(LAST_TRADING_DATE.plusDays(7))
        .build();
  }

}
