/* amodeus - Copyright (c) 2018, ETH Zurich, Institute for Dynamic Systems and Control */
package amodeus.amodeus.test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Objects;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.matsim.api.core.v01.network.Network;
import org.matsim.api.core.v01.population.Population;

import amodeus.amodeus.analysis.AnalysisConstants;
import amodeus.amodeus.analysis.StackedDistanceChartImage;
import amodeus.amodeus.analysis.element.BinnedWaitingTimesImage;
import amodeus.amodeus.analysis.element.DistanceDistributionOverDayImage;
import amodeus.amodeus.analysis.element.OccupancyDistanceRatiosImage;
import amodeus.amodeus.analysis.element.StatusDistributionImage;
import amodeus.amodeus.analysis.element.TravelHistory;
import amodeus.amodeus.options.ScenarioOptions;
import amodeus.amodeus.options.ScenarioOptionsBase;
import amodeus.amodeus.testutils.TestPreparer;
import amodeus.amodeus.testutils.TestServer;
import amodeus.amodeus.testutils.TestViewer;
import amodeus.amodeus.util.io.CopyFiles;
import amodeus.amodeus.util.io.Locate;
import amodeus.amodeus.util.io.MultiFileTools;
import amodeus.amodeus.util.math.GlobalAssert;
import amodeus.amodeus.util.math.SI;
import amodeus.amodeus.util.matsim.NetworkLoader;
import ch.ethz.idsc.tensor.RationalScalar;
import ch.ethz.idsc.tensor.RealScalar;
import ch.ethz.idsc.tensor.Scalar;
import ch.ethz.idsc.tensor.Scalars;
import ch.ethz.idsc.tensor.Tensor;
import ch.ethz.idsc.tensor.alg.Transpose;
import ch.ethz.idsc.tensor.io.UserName;
import ch.ethz.idsc.tensor.qty.Quantity;
import ch.ethz.idsc.tensor.qty.UnitConvert;
import ch.ethz.idsc.tensor.red.Total;
import ch.ethz.idsc.tensor.sca.Round;

@FixMethodOrder(MethodSorters.NAME_ASCENDING) // junit 5 would provide more elegant solution
public class ScenarioExecutionTest {
    private static final Scalar ZERO_KM = Quantity.of(0, "km");
    // ---
    private static TestServer testServer;

    @BeforeClass
    public static void setUpOnce() throws Exception {
        File workingDirectory = MultiFileTools.getDefaultWorkingDirectory();

        // copy scenario data into main directory
        File scenarioDirectory = //
                new File(Locate.repoFolder(ScenarioExecutionTest.class, "amodeus"), "resources/testScenario");
        GlobalAssert.that(workingDirectory.isDirectory());
        TestFileHandling.copyScnearioToMainDirectory(scenarioDirectory.getAbsolutePath(), //
                workingDirectory.getAbsolutePath());

        // copy LPOptions from other location to ensure no travel data object is created,
        // the dispatcher used in this test does not require it.
        File helperDirectory = //
                new File(Locate.repoFolder(ScenarioExecutionTest.class, "amodeus"), "resources/helperFiles");
        CopyFiles.now(helperDirectory.getAbsolutePath(), workingDirectory.getAbsolutePath(), //
                Collections.singletonList("LPOptions.properties"), true);

        testServer = new TestServer(workingDirectory);
    }

    @Test
    public void testA_Preparer() throws Exception {
        System.out.print("Preparer Test:\t");

        // run scenario preparer
        TestPreparer testPreparer = TestPreparer.run(testServer.getWorkingDirectory());

        // creation of files
        File preparedPopulationFile = new File("preparedPopulation.xml");
        assertTrue(preparedPopulationFile.isFile());

        File preparedNetworkFile = new File("preparedNetwork.xml");
        assertTrue(preparedNetworkFile.isFile());

        File config = new File("config.xml");
        assertTrue(config.isFile());

        // consistency of network (here no cutting)
        Network originalNetwork = NetworkLoader.fromConfigFile(testServer.getConfigFile());
        Network preparedNetwork = testPreparer.getPreparedNetwork();
        GlobalAssert.that(Objects.nonNull(originalNetwork));
        GlobalAssert.that(Objects.nonNull(preparedNetwork));
        assertEquals(preparedNetwork.getNodes().size(), originalNetwork.getNodes().size());
        assertEquals(preparedNetwork.getLinks().size(), originalNetwork.getLinks().size());

        // consistency of population
        Population population = testPreparer.getPreparedPopulation();
        assertEquals(250, population.getPersons().size());
    }

    @Test
    public void testB_Server() throws Exception {
        System.out.print("Server Test:\t");

        // run scenario server
        testServer.simulate();

        // scenario options
        File workingDirectory = MultiFileTools.getDefaultWorkingDirectory();
        ScenarioOptions scenarioOptions = new ScenarioOptions(workingDirectory, ScenarioOptionsBase.getDefault());

        assertEquals(new File(workingDirectory, "config.xml").getAbsolutePath(), scenarioOptions.getSimulationConfigName());
        assertEquals(new File(workingDirectory, "preparedNetwork").getAbsolutePath(), scenarioOptions.getPreparedNetworkName());
        assertEquals(new File(workingDirectory, "preparedPopulation").getAbsolutePath(), scenarioOptions.getPreparedPopulationName());

        // simulation objects should exist after simulation (simulation data)
        File simobj = new File("output/001/simobj/it.00");
        assertTrue(simobj.exists());
        assertEquals(109, simobj.listFiles().length);
        assertTrue(new File(simobj, "0000000/0000120.bin").exists());
        assertTrue(new File(simobj, "0108000/0108000.bin").exists());
    }

    @Test
    public void testC_Analysis() {
        System.out.print("Analysis Test:\t");

        AnalysisTestExport ate = testServer.getAnalysisTestExport();

        /** number of processed requests, 25 are same link requests and therefore are not covered by Amodeus */
        assertEquals(247, ate.getSimulationInformationElement().reqsize());

        /** fleet size */
        assertEquals(40, ate.getSimulationInformationElement().vehicleSize());
        assertEquals(40, ate.getDistancElement().getVehicleStatistics().size());

        /** status distribution, every row must equal the total of vehicles */
        Tensor distributionSum = Total.of(Transpose.of(ate.getStatusDistribution().statusTensor));
        distributionSum.flatten(-1).forEach(e -> //
        assertEquals(e, RealScalar.of(ate.getSimulationInformationElement().vehicleSize())));

        ScalarAssert scalarAssert = new ScalarAssert();

        /** fleet distances */
        assertTrue(Scalars.lessEquals(ZERO_KM, ate.getDistancElement().totalDistance));
        assertTrue(Scalars.lessEquals(ZERO_KM, ate.getDistancElement().totalDistanceWtCst));
        assertTrue(Scalars.lessEquals(ZERO_KM, ate.getDistancElement().totalDistancePicku));
        assertTrue(Scalars.lessEquals(ZERO_KM, ate.getDistancElement().totalDistanceRebal));
        assertTrue(Scalars.lessEquals(RealScalar.ZERO, ate.getDistancElement().totalDistanceRatio));
        ate.getDistancElement().totalDistancesPerVehicle.flatten(-1).forEach(s -> //
        assertTrue(Scalars.lessEquals(ZERO_KM, (Scalar) s)));
        assertEquals(Total.of(ate.getDistancElement().totalDistancesPerVehicle), ate.getDistancElement().totalDistance);

        scalarAssert.add((Scalar) Quantity.of(2836.14438, "km").map(Round._5), (Scalar) ate.getDistancElement().totalDistance.map(Round._5));
        scalarAssert.add((Scalar) Quantity.of(1043.41812, "km").map(Round._5), (Scalar) ate.getDistancElement().totalDistanceWtCst.map(Round._5));
        scalarAssert.add((Scalar) Quantity.of(272.89791, "km").map(Round._5), (Scalar) ate.getDistancElement().totalDistancePicku.map(Round._5));
        scalarAssert.add((Scalar) Quantity.of(1519.82835, "km").map(Round._5), (Scalar) ate.getDistancElement().totalDistanceRebal.map(Round._5));
        scalarAssert.add((Scalar) RealScalar.of(0.36790).map(Round._5), (Scalar) ate.getDistancElement().totalDistanceRatio.map(Round._5));

        scalarAssert.add((Scalar) Total.of(ate.getDistancElement().totalDistancesPerVehicle), //
                ate.getDistancElement().totalDistance);

        /** travel time history */
        assertEquals(247, ate.getTravelTimeAnalysis().getTravelHistories().size());
        assertTrue(ate.getTravelTimeAnalysis().getTravelHistories().values().stream().map(TravelHistory::getDropOffTime) //
                .allMatch(s -> Scalars.lessEquals(s, UnitConvert.SI().to(SI.SECOND).apply(Quantity.of(30, "h")))));

        /** wait times, drive times */
        assertTrue(Scalars.lessEquals(Quantity.of(0, SI.SECOND), ate.getTravelTimeAnalysis().getWaitAggrgte().Get(2)));
        ate.getTravelTimeAnalysis().getWaitTimes().flatten(-1).forEach(t -> {
            Scalars.lessEquals(Quantity.of(0, SI.SECOND), (Quantity) t);
            Scalars.lessEquals((Quantity) t, ate.getTravelTimeAnalysis().getWaitAggrgte().Get(2));
        });

        assertTrue(Scalars.lessEquals(Quantity.of(0, SI.SECOND), ate.getTravelTimeAnalysis().getWaitAggrgte().get(0).Get(0)));
        assertTrue(Scalars.lessEquals(ate.getTravelTimeAnalysis().getWaitAggrgte().get(0).Get(0), //
                ate.getTravelTimeAnalysis().getWaitAggrgte().get(0).Get(1)));
        assertTrue(Scalars.lessEquals(ate.getTravelTimeAnalysis().getWaitAggrgte().get(0).Get(1), //
                ate.getTravelTimeAnalysis().getWaitAggrgte().get(0).Get(2)));
        assertTrue(Scalars.lessEquals(Quantity.of(0, SI.SECOND), ate.getTravelTimeAnalysis().getWaitAggrgte().Get(1)));

        scalarAssert.add(Quantity.of(396.01214574898785, SI.SECOND), ate.getTravelTimeAnalysis().getWaitAggrgte().Get(1));
        scalarAssert.add(Quantity.of(2397.0, SI.SECOND), ate.getTravelTimeAnalysis().getWaitAggrgte().Get(2));
        scalarAssert.add(Quantity.of(RationalScalar.of(222840, 247), SI.SECOND), ate.getTravelTimeAnalysis().getDrveAggrgte().Get(1));
        scalarAssert.add(Quantity.of(3480, SI.SECOND), ate.getTravelTimeAnalysis().getDrveAggrgte().Get(2));

        /* TODO @sebhoerl Have a look at {AmodeusModule::install}. At some point the travel time
         * calculation in DVRP has been improved.
         * Unfortunately, this improvement breaks these tests.
         * The reference numbers here should be adjusted at some point so that the fallback in
         * {AmodeusModule::install} can be removed again.
         * (Nevertheless, we're talking about a different in routed time of +/-1 second). /sebhoerl */
        scalarAssert.consolidate();

        /** presence of plot files */
        File data = new File("output/001/data");
        assertTrue(new File(data, BinnedWaitingTimesImage.FILE_PNG).isFile());
        assertTrue(new File(data, DistanceDistributionOverDayImage.FILE_PNG).isFile());
        assertTrue(new File(data, OccupancyDistanceRatiosImage.FILE_PNG).isFile());
        assertTrue(new File(data, StackedDistanceChartImage.FILE_PNG).isFile());
        assertTrue(new File(data, StatusDistributionImage.FILE_PNG).isFile());

        assertTrue(new File(data, AnalysisConstants.ParametersExportFilename).exists());

        assertTrue(new File(data, "WaitingTimes").isDirectory());
        assertTrue(new File(data, "WaitingTimes/WaitingTimes.mathematica").isFile());

        assertTrue(new File(data, "StatusDistribution").isDirectory());
        assertTrue(new File(data, "StatusDistribution/StatusDistribution.mathematica").isFile());

        assertTrue(new File(data, "DistancesOverDay").isDirectory());
        assertTrue(new File(data, "DistancesOverDay/DistancesOverDay.mathematica").isFile());

        assertTrue(new File(data, "DistanceRatios").isDirectory());
        assertTrue(new File(data, "DistanceRatios/DistanceRatios.mathematica").isFile());

        assertTrue(new File("output/001/data/requestHistory.csv").isFile());
        assertTrue(new File("output/001/data/vehicleActivitiesHistory.csv").isFile());
        assertTrue(new File("output/001/data/vehicleMovementsHistory.csv").isFile());

        assertTrue(new File("output/001/report/report.html").isFile());
        assertTrue(new File("output/001/report/config.xml").isFile());
    }

    @Test
    public void testD_Viewer() throws IOException {
        if (!UserName.is("travis")) {
            // run scenario viewer
            TestViewer testViewer = TestViewer.run(testServer.getWorkingDirectory());

            System.out.println("Viewer Test:");

            assertEquals(9, testViewer.getAmodeusComponent().viewerLayers.size());
            assertEquals(32, testViewer.getViewerConfig().settings.getClass().getFields().length);
        }
    }

    @AfterClass
    public static void tearDownOnce() throws IOException {
        TestFileHandling.removeGeneratedFiles();
    }
}