package com.tibbo.aggregate.common.expression.function;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import com.tibbo.aggregate.common.datatable.DataTable;
import com.tibbo.aggregate.common.datatable.FieldFormat;
import com.tibbo.aggregate.common.datatable.SimpleDataTable;
import com.tibbo.aggregate.common.datatable.TableFormat;
import com.tibbo.aggregate.common.expression.EvaluationException;
import com.tibbo.aggregate.common.expression.Evaluator;
import com.tibbo.aggregate.common.expression.function.math.AverageFunction;
import com.tibbo.aggregate.common.expression.function.math.FrequencyFunction;
import com.tibbo.aggregate.common.expression.function.math.LinearRegressionFunction;
import com.tibbo.aggregate.common.expression.function.math.MaxFunction;
import com.tibbo.aggregate.common.expression.function.math.MedianFunction;
import com.tibbo.aggregate.common.expression.function.math.MinFunction;
import com.tibbo.aggregate.common.expression.function.math.ModeFunction;
import com.tibbo.aggregate.common.expression.function.math.SimpleMovingAverageFunction;
import com.tibbo.aggregate.common.expression.function.math.SimpleMovingMedianFunction;
import com.tibbo.aggregate.common.expression.function.math.StandardDeviationFunction;
import com.tibbo.aggregate.common.expression.function.math.StandardErrorMeanFunction;
import com.tibbo.aggregate.common.expression.function.math.SumFunction;
import com.tibbo.aggregate.common.expression.function.math.VarianceFunction;
import com.tibbo.aggregate.common.tests.CommonsFixture;

public class TestMathematicalFunctions
{
  private Evaluator ev;
  
  @BeforeEach
  public void setUp() throws Exception
  {
    ev = CommonsFixture.createTestEvaluator();
  }
  
  @Test
  public void testAverageFunction() throws Exception
  {
    DataTable defaultTable = ev.getDefaultResolver().getDefaultTable();
    double expected = 192.0;
    
    Double resultByFieldName = (Double) new AverageFunction().execute(null, null, defaultTable, "int");
    assertEquals(expected, resultByFieldName, 0.1);
    
    Double resultByFieldIndex = (Double) new AverageFunction().execute(null, null, defaultTable, 1);
    assertEquals(expected, resultByFieldIndex, 0.1);
    
    int nonExistingFieldIndex = defaultTable.getFieldCount();
    assertThrows(EvaluationException.class, () -> new AverageFunction().execute(null, null, defaultTable, nonExistingFieldIndex));
    
    String nonExistingFieldName = "nonExistingFieldName";
    assertThrows(EvaluationException.class, () -> new AverageFunction().execute(null, null, defaultTable, nonExistingFieldName));
    
    int fieldIndexWithNonNumberType = 0; // string
    assertThrows(EvaluationException.class, () -> new AverageFunction().execute(null, null, defaultTable, fieldIndexWithNonNumberType));
  }
  
  @Test
  public void testDispersionFunction() throws Exception
  {
    DataTable defaultTable = ev.getDefaultResolver().getDefaultTable();
    double expected = 76579.3;
    
    Double result = (Double) new VarianceFunction().execute(null, null, defaultTable, "int");
    
    assertEquals(expected, result, 0.1);
  }
  
  @Test
  public void testFrequencyFunction() throws Exception
  {
    DataTable defaultTable = ev.getDefaultResolver().getDefaultTable();
    Long expected = 1L;
    
    DataTable result = (DataTable) new FrequencyFunction().execute(null, null, defaultTable, "int");
    
    assertEquals(expected, result.getRecord(0).getLong(1));
  }
  
  @Test
  public void testLinearRegressionFunction() throws Exception
  {
    TableFormat tableFormat = new TableFormat();
    tableFormat.addField(FieldFormat.create("x", FieldFormat.DOUBLE_FIELD));
    tableFormat.addField(FieldFormat.create("y", FieldFormat.DOUBLE_FIELD));
    DataTable table = new SimpleDataTable(tableFormat);
    table.addRecord(1, 3);
    table.addRecord(2, 4);
    table.addRecord(3, 5);
    double expected1 = 1.0;
    double expected2 = 2.0;
    
    DataTable result = (DataTable) new LinearRegressionFunction().execute(null, null, table, "x", "y");
    
    assertEquals(expected1, result.getRecord(0).getDouble(0), 0.1);
    assertEquals(expected2, result.getRecord(0).getDouble(1), 0.1);
  }
  
  @Test
  public void testMaxFunction() throws Exception
  {
    DataTable defaultTable = ev.getDefaultResolver().getDefaultTable();
    double expected = 666.0;
    
    Double result = (Double) new MaxFunction().execute(null, null, defaultTable, "int");
    
    assertEquals(expected, result, 0.1);
  }
  
  @Test
  public void testMaxFunctionWithTwoNumericArguments() throws Exception
  {
    double expected = 2.0;
    
    Double result = (Double) new MaxFunction().execute(null, null, 1, 2);
    
    assertEquals(expected, result, 0.1);
  }
  
  @Test
  public void testMedianFunction() throws Exception
  {
    DataTable defaultTable = ev.getDefaultResolver().getDefaultTable();
    double expected = 123.0;
    
    Double result = (Double) new MedianFunction().execute(null, null, defaultTable, "int");
    
    assertEquals(expected, result, 0.1);
  }
  
  @Test
  public void testMinFunction() throws Exception
  {
    DataTable defaultTable = ev.getDefaultResolver().getDefaultTable();
    double expected = -111.0;
    
    Double result = (Double) new MinFunction().execute(null, null, defaultTable, "int");
    
    assertEquals(expected, result, 0.1);
  }
  
  @Test
  public void testMinFunctionWithTwoNumericArguments() throws Exception
  {
    double expected = 1.0;
    
    Double result = (Double) new MinFunction().execute(null, null, 1, 2);
    
    assertEquals(expected, result, 0.1);
  }
  
  @Test
  public void testModeFunction() throws Exception
  {
    DataTable defaultTable = ev.getDefaultResolver().getDefaultTable();
    double expected = -111.0;
    
    Double result = (Double) new ModeFunction().execute(null, null, defaultTable, "int");
    
    assertEquals(expected, result, 0.1);
  }
  
  @Test
  public void testSimpleMovingAverageFunction() throws Exception
  {
    DataTable defaultTable = ev.getDefaultResolver().getDefaultTable();
    double expected = 172.5;
    
    DataTable result = (DataTable) new SimpleMovingAverageFunction().execute(null, null, defaultTable, "int", 2);
    
    assertEquals(expected, result.getRecord(1).getDouble(1), 0.1);
  }
  
  @Test
  public void testSimpleMovingMedianFunction() throws Exception
  {
    DataTable defaultTable = ev.getDefaultResolver().getDefaultTable();
    double expected = 172.5;
    
    DataTable result = (DataTable) new SimpleMovingMedianFunction().execute(null, null, defaultTable, "int", 2);
    
    assertEquals(expected, result.getRecord(1).getDouble(1), 0.1);
  }
  
  @Test
  public void testStandardDeviationFunction() throws Exception
  {
    DataTable defaultTable = ev.getDefaultResolver().getDefaultTable();
    double expected = 276.7;
    
    Double result = (Double) new StandardDeviationFunction().execute(null, null, defaultTable, "int");
    
    assertEquals(expected, result, 0.1);
  }
  
  @Test
  public void testStandardErrorMeanFunction() throws Exception
  {
    DataTable defaultTable = ev.getDefaultResolver().getDefaultTable();
    double expected = 256.2;
    
    Double result = (Double) new StandardErrorMeanFunction().execute(null, null, defaultTable, "int");
    
    assertEquals(expected, result, 0.1);
  }
  
  @Test
  public void testSumFunction() throws Exception
  {
    DataTable defaultTable = ev.getDefaultResolver().getDefaultTable();
    Double expected = 1344.0;
    
    Double result = (Double) new SumFunction().execute(null, null, defaultTable, "int");
    
    assertEquals(expected, result);
  }
  
  @Test
  public void testSumFunctionWithNullValues() throws Exception
  {
    TableFormat tableFormat = new TableFormat();
    tableFormat.addField(FieldFormat.create("int", FieldFormat.DOUBLE_FIELD).setNullable(true));
    DataTable table = new SimpleDataTable(tableFormat);
    table.addRecord(1);
    table.addRecord(1);
    
    table.addRecord().setValue("int", null);
    Double expected = 2.0;
    
    Double result = (Double) new SumFunction().execute(null, null, table, "int");
    
    assertEquals(expected, result);
  }
  
  @Test
  public void testSimultaneouslyExecution() throws Exception
  {
    TableFormat tableFormat = new TableFormat();
    tableFormat.addField(FieldFormat.create("mass_w", FieldFormat.DOUBLE_FIELD));
    tableFormat.addField(FieldFormat.create("mass", FieldFormat.DOUBLE_FIELD));
    DataTable table = new SimpleDataTable(tableFormat);
    table.addRecord(5.0, 4.75);
    
    SumFunction function = new SumFunction();
    
    Double expected1 = 5.0;
    Double expected2 = 4.75;
    
    ExecutorService executor = Executors.newFixedThreadPool(2);
    
    try
    {
      for (int i = 0; i < 20; i++)
      {
        Future<Double> future1 = executor.submit(() -> (Double) function.execute(null, null, table, "mass_w"));
        
        Future<Double> future2 = executor.submit(() -> (Double) function.execute(null, null, table, "mass"));
        
        Double result1 = future1.get();
        Double result2 = future2.get();
        
        assertEquals(expected1, result1);
        assertEquals(expected2, result2);
      }
    }
    finally
    {
      executor.shutdown();
    }
  }
}