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

import static com.opengamma.strata.collect.TestHelper.date;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.Arrays;
import java.util.Map;
import java.util.Objects;

import org.junit.jupiter.api.Test;

import com.google.common.collect.ImmutableMap;
import com.opengamma.strata.basics.ReferenceData;
import com.opengamma.strata.basics.StandardId;
import com.opengamma.strata.collect.tuple.Pair;
import com.opengamma.strata.data.FieldName;
import com.opengamma.strata.data.MarketDataId;
import com.opengamma.strata.data.ObservableId;
import com.opengamma.strata.data.ObservableSource;
import com.opengamma.strata.data.scenario.MarketDataBox;
import com.opengamma.strata.data.scenario.ScenarioMarketData;

public class MarketDataNodeTest {

  /**
   * Tests removing the leaf dependencies from the tree to decide which market data can be built.
   */
  @Test
  public void withLeavesRemoved() {
    MarketDataNode root =
        rootNode(
            observableNode(new TestIdA("1")),
            valueNode(
                new TestIdB("2"),
                valueNode(new TestIdB("3")),
                observableNode(new TestIdA("4")),
                valueNode(
                    new TestIdB("5"),
                    timeSeriesNode(new TestIdA("6")))),
            valueNode(new TestIdB("7")));

    Pair<MarketDataNode, MarketDataRequirements> pair1 = root.withLeavesRemoved();

    MarketDataRequirements expectedReqs1 =
        MarketDataRequirements.builder()
            .addValues(new TestIdA("1"))
            .addValues(new TestIdB("3"))
            .addValues(new TestIdA("4"))
            .addTimeSeries(new TestIdA("6"))
            .addValues(new TestIdB("7"))
            .build();

    MarketDataNode expectedTree1 =
        rootNode(
            valueNode(
                new TestIdB("2"),
                valueNode(
                    new TestIdB("5"))));

    MarketDataNode tree1 = pair1.getFirst();
    MarketDataRequirements reqs1 = pair1.getSecond();

    assertThat(tree1).isEqualTo(expectedTree1);
    assertThat(expectedReqs1).isEqualTo(reqs1);

    Pair<MarketDataNode, MarketDataRequirements> pair2 = tree1.withLeavesRemoved();

    MarketDataRequirements expectedReqs2 =
        MarketDataRequirements.builder()
            .addValues(new TestIdB("5"))
            .build();

    MarketDataNode expectedTree2 =
        rootNode(
            valueNode(
                new TestIdB("2")));

    MarketDataNode tree2 = pair2.getFirst();
    MarketDataRequirements reqs2 = pair2.getSecond();

    assertThat(tree2).isEqualTo(expectedTree2);
    assertThat(expectedReqs2).isEqualTo(reqs2);

    Pair<MarketDataNode, MarketDataRequirements> pair3 = tree2.withLeavesRemoved();

    MarketDataRequirements expectedReqs3 =
        MarketDataRequirements.builder()
            .addValues(new TestIdB("2"))
            .build();

    MarketDataNode tree3 = pair3.getFirst();
    MarketDataRequirements reqs3 = pair3.getSecond();

    assertThat(tree3.isLeaf()).isTrue();
    assertThat(expectedReqs3).isEqualTo(reqs3);
  }

  /**
   * Tests building a tree of requirements using market data functions.
   */
  @Test
  public void buildDependencyTree() {
    MarketDataNode expected =
        rootNode(
            observableNode(new TestIdA("1")),
            valueNode(
                new TestIdB("2"),
                valueNode(
                    new TestIdB("4"),
                    observableNode(new TestIdA("5"))),
                timeSeriesNode(new TestIdA("3"))),
            timeSeriesNode(new TestIdA("6")));

    // The requirements for the data directly used by the calculations
    MarketDataRequirements requirements =
        MarketDataRequirements.builder()
            .addValues(new TestIdA("1"), new TestIdB("2"))
            .addTimeSeries(new TestIdA("6"))
            .build();

    // Requirements for each item in the tree - used to initialize the functions
    MarketDataRequirements id2Reqs =
        MarketDataRequirements.builder()
            .addTimeSeries(new TestIdA("3"))
            .addValues(new TestIdB("4"))
            .build();

    MarketDataRequirements id4Reqs =
        MarketDataRequirements.builder()
            .addValues(new TestIdA("5"))
            .build();

    ImmutableMap<TestIdB, MarketDataRequirements> reqsMap =
        ImmutableMap.of(
            new TestIdB("2"), id2Reqs,
            new TestIdB("4"), id4Reqs);

    TestMarketDataFunctionA builderA = new TestMarketDataFunctionA();
    TestMarketDataFunctionB builderB = new TestMarketDataFunctionB(reqsMap);

    ImmutableMap<Class<? extends MarketDataId<?>>, MarketDataFunction<?, ?>> functions =
        ImmutableMap.of(
            TestIdA.class, builderA,
            TestIdB.class, builderB);

    MarketDataNode root =
        MarketDataNode.buildDependencyTree(
            requirements,
            BuiltScenarioMarketData.empty(),
            MarketDataConfig.empty(),
            functions);

    assertThat(root).isEqualTo(expected);
  }

  /**
   * Tests that supplied data is in a leaf node and the functions aren't asked for dependencies for supplied data.
   */
  @Test
  public void noDependenciesForSuppliedData() {
    MarketDataNode expected1 =
        rootNode(
            valueNode(
                new TestIdB("1"),
                observableNode(new TestIdA("2"))),
            valueNode(
                new TestIdB("3"),
                valueNode(new TestIdB("4"))));

    MarketDataRequirements requirements =
        MarketDataRequirements.builder()
            .addValues(new TestIdB("1"), new TestIdB("3"))
            .build();

    MarketDataRequirements id1Reqs =
        MarketDataRequirements.builder()
            .addValues(new TestIdA("2"))
            .build();

    MarketDataRequirements id3Reqs =
        MarketDataRequirements.builder()
            .addValues(new TestIdB("4"))
            .build();

    ImmutableMap<TestIdB, MarketDataRequirements> reqsMap =
        ImmutableMap.of(
            new TestIdB("1"), id1Reqs,
            new TestIdB("3"), id3Reqs);

    TestMarketDataFunctionB builder = new TestMarketDataFunctionB(reqsMap);

    ImmutableMap<Class<? extends MarketDataId<?>>, MarketDataFunction<?, ?>> functions =
        ImmutableMap.of(
            TestIdB.class, builder);

    MarketDataNode root1 =
        MarketDataNode.buildDependencyTree(
            requirements,
            BuiltScenarioMarketData.empty(),
            MarketDataConfig.empty(),
            functions);

    assertThat(root1).isEqualTo(expected1);

    BuiltScenarioMarketData suppliedData =
        BuiltScenarioMarketData.builder(date(2011, 3, 8))
            .addValue(new TestIdB("1"), new TestMarketDataB())
            .addValue(new TestIdB("3"), new TestMarketDataB())
            .build();

    MarketDataNode root2 =
        MarketDataNode.buildDependencyTree(
            requirements,
            suppliedData,
            MarketDataConfig.empty(),
            functions);

    MarketDataNode expected2 =
        rootNode(
            valueNode(new TestIdB("1")),
            valueNode(new TestIdB("3")));

    assertThat(root2).isEqualTo(expected2);
  }

  /**
   * Test a node with no children is added when there is no market data function for an ID.
   */
  @Test
  public void noMarketDataBuilder() {
    MarketDataNode expected =
        rootNode(
            valueNode(new TestIdC("1")),
            valueNode(
                new TestIdB("2"),
                valueNode(new TestIdC("3"))));

    MarketDataRequirements requirements =
        MarketDataRequirements.builder()
            .addValues(new TestIdC("1"), new TestIdB("2"))
            .build();

    MarketDataRequirements id2Reqs =
        MarketDataRequirements.builder()
            .addValues(new TestIdC("3"))
            .build();

    TestMarketDataFunctionB builder = new TestMarketDataFunctionB(ImmutableMap.of(new TestIdB("2"), id2Reqs));
    ImmutableMap<Class<? extends MarketDataId<?>>, MarketDataFunction<?, ?>> functions =
        ImmutableMap.of(TestIdB.class, builder);
    // Build the tree without providing a market data function to handle TestId3
    MarketDataNode root =
        MarketDataNode.buildDependencyTree(
            requirements,
            BuiltScenarioMarketData.empty(),
            MarketDataConfig.empty(),
            functions);

    assertThat(root).isEqualTo(expected);
  }

  //-------------------------------------------------------------------------
  private static MarketDataNode rootNode(MarketDataNode... children) {
    return MarketDataNode.root(Arrays.asList(children));
  }

  private static MarketDataNode valueNode(MarketDataId<?> id, MarketDataNode... children) {
    return MarketDataNode.child(id, MarketDataNode.DataType.SINGLE_VALUE, Arrays.asList(children));
  }

  private static MarketDataNode observableNode(ObservableId id) {
    return MarketDataNode.leaf(id, MarketDataNode.DataType.SINGLE_VALUE);
  }

  private static MarketDataNode timeSeriesNode(ObservableId id) {
    return MarketDataNode.leaf(id, MarketDataNode.DataType.TIME_SERIES);
  }

  class TestIdA implements ObservableId {

    private final StandardId id;

    TestIdA(String id) {
      this.id = StandardId.of("test", id);
    }

    @Override
    public StandardId getStandardId() {
      return id;
    }

    @Override
    public FieldName getFieldName() {
      return FieldName.MARKET_VALUE;
    }

    @Override
    public ObservableSource getObservableSource() {
      return ObservableSource.NONE;
    }

    @Override
    public ObservableId withObservableSource(ObservableSource obsSource) {
      return this;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
      TestIdA idA = (TestIdA) o;
      return Objects.equals(id, idA.id);
    }

    @Override
    public int hashCode() {
      return Objects.hash(id);
    }

    @Override
    public String toString() {
      return "TestId1 [id=" + id + "]";
    }
  }

  class TestIdB implements MarketDataId<TestMarketDataB> {

    private final String str;

    TestIdB(String str) {
      this.str = str;
    }

    @Override
    public Class<TestMarketDataB> getMarketDataType() {
      return TestMarketDataB.class;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
      TestIdB idB = (TestIdB) o;
      return Objects.equals(str, idB.str);
    }

    @Override
    public int hashCode() {
      return Objects.hash(str);
    }

    @Override
    public String toString() {
      return "TestId2 [str='" + str + "']";
    }
  }

  private static final class TestIdC implements MarketDataId<String> {

    private final String id;

    private TestIdC(String id) {
      this.id = id;
    }

    @Override
    public Class<String> getMarketDataType() {
      return String.class;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
      TestIdC idC = (TestIdC) o;
      return Objects.equals(id, idC.id);
    }

    @Override
    public int hashCode() {
      return Objects.hash(id);
    }

    @Override
    public String toString() {
      return "BazId [id='" + id + "']";
    }
  }

  private static final class TestMarketDataFunctionA implements MarketDataFunction<Double, TestIdA> {

    @Override
    public MarketDataRequirements requirements(TestIdA id, MarketDataConfig marketDataConfig) {
      // The ID represents observable data which has no dependencies by definition
      return MarketDataRequirements.empty();
    }

    @Override
    public MarketDataBox<Double> build(
        TestIdA id,
        MarketDataConfig marketDataConfig,
        ScenarioMarketData marketData,
        ReferenceData refData) {

      throw new UnsupportedOperationException("build not implemented");
    }

    @Override
    public Class<TestIdA> getMarketDataIdType() {
      return TestIdA.class;
    }
  }

  private static final class TestMarketDataB {
  }

  private static final class TestMarketDataFunctionB implements MarketDataFunction<TestMarketDataB, TestIdB> {

    private final Map<TestIdB, MarketDataRequirements> requirements;

    private TestMarketDataFunctionB(Map<TestIdB, MarketDataRequirements> requirements) {
      this.requirements = requirements;
    }

    @Override
    public MarketDataRequirements requirements(TestIdB id, MarketDataConfig marketDataConfig) {
      return requirements.getOrDefault(id, MarketDataRequirements.empty());
    }

    @Override
    public MarketDataBox<TestMarketDataB> build(
        TestIdB id,
        MarketDataConfig marketDataConfig,
        ScenarioMarketData marketData,
        ReferenceData refData) {

      throw new UnsupportedOperationException("build not implemented");
    }

    @Override
    public Class<TestIdB> getMarketDataIdType() {
      return TestIdB.class;
    }
  }
}
