CracImportSteps.java

/*
 * Copyright (c) 2024, RTE (http://www.rte-france.com)
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
package com.powsybl.openrao.tests.steps;

import com.powsybl.action.*;
import com.powsybl.contingency.Contingency;
import com.powsybl.iidm.network.Country;
import com.powsybl.iidm.network.TwoSides;
import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.commons.Unit;
import com.powsybl.openrao.data.crac.api.Crac;
import com.powsybl.openrao.data.crac.api.Identifiable;
import com.powsybl.openrao.data.crac.api.Instant;
import com.powsybl.openrao.data.crac.api.InstantKind;
import com.powsybl.openrao.data.crac.api.NetworkElement;
import com.powsybl.openrao.data.crac.api.RemedialAction;
import com.powsybl.openrao.data.crac.api.cnec.AngleCnec;
import com.powsybl.openrao.data.crac.api.cnec.BranchCnec;
import com.powsybl.openrao.data.crac.api.cnec.Cnec;
import com.powsybl.openrao.data.crac.api.cnec.FlowCnec;
import com.powsybl.openrao.data.crac.api.cnec.VoltageCnec;
import com.powsybl.openrao.data.crac.api.usagerule.OnConstraint;
import com.powsybl.openrao.data.crac.api.usagerule.OnContingencyState;
import com.powsybl.openrao.data.crac.api.usagerule.OnFlowConstraintInCountry;
import com.powsybl.openrao.data.crac.api.usagerule.OnInstant;
import com.powsybl.openrao.data.crac.api.usagerule.UsageMethod;
import com.powsybl.openrao.data.crac.api.networkaction.ActionType;
import com.powsybl.openrao.data.crac.api.networkaction.NetworkAction;
import com.powsybl.openrao.data.crac.api.networkaction.SwitchPair;
import com.powsybl.openrao.data.crac.api.range.RangeType;
import com.powsybl.openrao.data.crac.api.range.StandardRange;
import com.powsybl.openrao.data.crac.api.rangeaction.HvdcRangeAction;
import com.powsybl.openrao.data.crac.api.rangeaction.InjectionRangeAction;
import com.powsybl.openrao.data.crac.api.rangeaction.PstRangeAction;
import com.powsybl.openrao.data.crac.api.rangeaction.RangeAction;
import com.powsybl.openrao.data.crac.api.threshold.BranchThreshold;
import com.powsybl.openrao.data.crac.api.CracCreationContext;
import com.powsybl.openrao.data.crac.io.commons.api.ElementaryCreationContext;
import com.powsybl.openrao.data.crac.io.commons.api.ImportStatus;
import com.powsybl.openrao.data.crac.io.commons.api.stdcreationcontext.BranchCnecCreationContext;
import com.powsybl.openrao.data.crac.io.commons.api.stdcreationcontext.UcteCracCreationContext;
import com.powsybl.openrao.data.crac.io.cse.CseCracCreationContext;
import com.powsybl.openrao.data.crac.io.fbconstraint.FbConstraintCreationContext;
import io.cucumber.datatable.DataTable;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import org.apache.commons.lang3.NotImplementedException;

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

import static org.junit.jupiter.api.Assertions.*;

/**
 * @author Marjorie Cosson {@literal <marjorie.cosson at rte-france.com>}
 * @author Alexandre Montigny {@literal <alexandre.montigny at rte-france.com>}
 */
public class CracImportSteps {

    private static final double DOUBLE_TOLERANCE = 1e-1;
    private static final String NOT_IMPLEMENTED_JSON = "This step is not implemented for open-rao-native crac import";
    private static final String NOT_IMPLEMENTED_FB = "This step is not implemented for FbConstraintCreationContext";
    private static final String TYPE_NOT_HANDLED = "%s type is not handled";
    private Crac crac;
    private CracCreationContext cracCreationContext;

    @When("I import crac")
    public void iImportCrac() throws IOException {
        importData(null);
    }

    @When("I import crac at {string}")
    public void iImportCrac(String timestamp) throws IOException {
        importData(timestamp);
    }

    private void importData(String timestamp) throws IOException {
        CommonTestData.loadData(timestamp);
        this.crac = CommonTestData.getCrac();
        this.cracCreationContext = CommonTestData.getCracCreationContext();
    }

    @Then("its name should be {string}")
    public void itsNameShouldBe(String expectedName) {
        assertEquals(expectedName, crac.getName());
    }

    @Then("its id should be {string}")
    public void itsIdShouldBe(String expectedId) {
        assertEquals(expectedId, crac.getId());
    }

    @Then("all the contingencies should be imported successfully")
    public void allContingenciesMustBeImported() {
        if (cracCreationContext == null) {
            throw new NotImplementedException(NOT_IMPLEMENTED_JSON);
        } else if (cracCreationContext instanceof FbConstraintCreationContext) {
            throw new NotImplementedException(NOT_IMPLEMENTED_FB);
        } else if (cracCreationContext instanceof CseCracCreationContext cseCracCreationContext) {
            assertTrue(cseCracCreationContext.getOutageCreationContexts().stream().allMatch(ElementaryCreationContext::isImported));
        } else {
            throw new NotImplementedException(String.format(TYPE_NOT_HANDLED, cracCreationContext.getClass().getName()));
        }
    }

    @Then("the native contingency {string} should create the contingency {string}")
    public void mapContingency(String nativeContingencyId, String createdContingencyId) {
        if (cracCreationContext == null) {
            throw new NotImplementedException(NOT_IMPLEMENTED_JSON);
        } else if (cracCreationContext instanceof FbConstraintCreationContext) {
            throw new NotImplementedException(NOT_IMPLEMENTED_FB);
        } else if (cracCreationContext instanceof CseCracCreationContext cseCracCreationContext) {
            ElementaryCreationContext creationContext = cseCracCreationContext.getOutageCreationContext(nativeContingencyId);
            assertNotNull(creationContext);
            assertTrue(creationContext.isImported());
            assertEquals(createdContingencyId, creationContext.getNativeObjectName());
        } else {
            throw new NotImplementedException(String.format(TYPE_NOT_HANDLED, cracCreationContext.getClass().getName()));
        }
    }

    @Then("the native contingency {string} should not be imported")
    public void contingencyShouldNotBeImported(String nativeContingencyId) {
        if (cracCreationContext == null) {
            throw new NotImplementedException(NOT_IMPLEMENTED_JSON);
        } else if (cracCreationContext instanceof FbConstraintCreationContext) {
            throw new NotImplementedException(NOT_IMPLEMENTED_FB);
        } else if (cracCreationContext instanceof CseCracCreationContext cseCracCreationContext) {
            ElementaryCreationContext creationContext = cseCracCreationContext.getOutageCreationContext(nativeContingencyId);
            assertNotNull(creationContext);
            assertFalse(creationContext.isImported());
            assertNull(creationContext.getCreatedObjectId());
        } else {
            throw new NotImplementedException(String.format(TYPE_NOT_HANDLED, cracCreationContext.getClass().getName()));
        }
    }

    @Then("it should have {int} contingencies")
    public void itShouldHaveContingencies(int expectedContingencyNumber) {
        assertEquals(expectedContingencyNumber, crac.getContingencies().size());
    }

    @Then("it should have the following contingencies:")
    public void itShouldHaveTheFollowingContingencies(DataTable arg1) {
        List<Map<String, String>> expectedContingencies = arg1.asMaps(String.class, String.class);
        int number = expectedContingencies.stream().map(stringStringMap -> stringStringMap.get("ContingencyId")).collect(Collectors.toSet()).size();
        assertEquals(number, crac.getContingencies().size());
        for (Map<String, String> expectedContingency : expectedContingencies) {
            Contingency contingency = crac.getContingency(expectedContingency.get("ContingencyId"));
            assertNotNull(contingency);
            assertEquals(Optional.of(expectedContingency.get("ContingencyName")), contingency.getName());
            assertEquals(Integer.parseInt(expectedContingency.get("NetworkElements")), contingency.getElements().size());
            assertTrue(contingency.getElements().stream().anyMatch(ne -> ne.getId().equals(expectedContingency.get("NetworkElementId"))));
        }
    }

    @Then("all the CNECs should be imported successfully")
    public void allCnecsMustBeImported() {
        if (cracCreationContext == null) {
            throw new NotImplementedException(NOT_IMPLEMENTED_JSON);
        } else if (cracCreationContext instanceof UcteCracCreationContext ucteCracCreationContext) {
            assertTrue(ucteCracCreationContext.getBranchCnecCreationContexts().stream().allMatch(BranchCnecCreationContext::isImported));
        } else {
            throw new NotImplementedException(String.format(TYPE_NOT_HANDLED, cracCreationContext.getClass().getName()));
        }
    }

    @Then("the native CNEC {string} from {string} to {string} with suffix {string} should create the CNEC {string} at instant {string}")
    public void mapNativeCnec(String nativeCnecId, String nativeFrom, String nativeTo, String nativeSuffix, String createdCnecId, String instantId) {
        if (cracCreationContext == null) {
            throw new NotImplementedException(NOT_IMPLEMENTED_JSON);
        } else if (cracCreationContext instanceof UcteCracCreationContext ucteCracCreationContext) {
            BranchCnecCreationContext branchCnecCreationContext = ucteCracCreationContext.getBranchCnecCreationContext(nativeCnecId);
            assertNotNull(branchCnecCreationContext);
            assertTrue(branchCnecCreationContext.isImported());
            assertEquals(nativeFrom, branchCnecCreationContext.getNativeBranch().getFrom());
            assertEquals(nativeTo, branchCnecCreationContext.getNativeBranch().getTo());
            assertEquals(nativeSuffix, branchCnecCreationContext.getNativeBranch().getSuffix());
            assertEquals(createdCnecId, branchCnecCreationContext.getCreatedCnecsIds().get(instantId.toLowerCase()));
        } else {
            throw new NotImplementedException(String.format("%s type is not handled by this step", cracCreationContext.getClass().getName()));
        }
    }

    @Then("the native CNEC {string} should create the CNEC {string} at instant {string}")
    public void mapNativeCnec(String nativeCnecId, String createdCnecId, String instantId) {
        if (cracCreationContext == null) {
            throw new NotImplementedException(NOT_IMPLEMENTED_JSON);
        } else if (cracCreationContext instanceof UcteCracCreationContext ucteCracCreationContext) {
            BranchCnecCreationContext branchCnecCreationContext = ucteCracCreationContext.getBranchCnecCreationContext(nativeCnecId);
            assertNotNull(branchCnecCreationContext);
            assertTrue(branchCnecCreationContext.isImported());
            assertEquals(createdCnecId, branchCnecCreationContext.getCreatedCnecsIds().get(instantId.toLowerCase()));
        } else {
            throw new NotImplementedException(String.format(TYPE_NOT_HANDLED, cracCreationContext.getClass().getName()));
        }
    }

    @Then("the native CNEC {string} should not be imported")
    public void cnecShouldNotBeImported(String nativeCnecId) {
        if (cracCreationContext == null) {
            throw new NotImplementedException(NOT_IMPLEMENTED_JSON);
        } else if (cracCreationContext instanceof UcteCracCreationContext ucteCracCreationContext) {
            BranchCnecCreationContext branchCnecCreationContext = ucteCracCreationContext.getBranchCnecCreationContext(nativeCnecId);
            assertNotNull(branchCnecCreationContext);
            assertFalse(branchCnecCreationContext.isImported());
            assertTrue(branchCnecCreationContext.getCreatedCnecsIds().isEmpty());
        } else {
            throw new NotImplementedException(String.format(TYPE_NOT_HANDLED, cracCreationContext.getClass().getName()));
        }
    }

    @Then("it should have the following flow CNECs:")
    public void itShouldHaveTheFollowingCNECs(DataTable arg1) throws Exception {
        List<Map<String, String>> expectedCnecs = arg1.asMaps(String.class, String.class);
        assertEquals(expectedCnecs.size(), crac.getFlowCnecs().size());
        for (Map<String, String> expectedCnec : expectedCnecs) {
            String cnecName = expectedCnec.get("Name");
            String networkElementId = expectedCnec.get("NetworkElementId");
            String contingency = expectedCnec.get("Contingency");
            Instant instant = crac.getInstant(expectedCnec.get("Instant").toLowerCase());
            boolean optimized = expectedCnec.get("Optimized").equalsIgnoreCase("yes");
            boolean monitored = expectedCnec.get("Monitored").equalsIgnoreCase("yes");

            Set<FlowCnec> cnecs;
            if (instant.isPreventive()) {
                cnecs = crac.getFlowCnecs(crac.getPreventiveState());
            } else {
                cnecs = crac.getFlowCnecs(crac.getState(contingency, instant));
            }
            FlowCnec flowCnec = cnecs.stream()
                .filter(cnec -> cnec.getName()
                    .equals(cnecName))
                .filter(cnec -> cnec.getNetworkElement().getId()
                    .equals(networkElementId))
                .filter(cnec -> cnec.isOptimized() == optimized)
                .filter(cnec -> cnec.isMonitored() == monitored)
                .findFirst()
                .orElseThrow(Exception::new);

            if (expectedCnec.get("ImaxLeft") != null) {
                assertEquals(Double.parseDouble(expectedCnec.get("ImaxLeft")), flowCnec.getIMax(TwoSides.ONE), DOUBLE_TOLERANCE);
            } else {
                assertNull(flowCnec.getIMax(TwoSides.ONE));
            }
            if (expectedCnec.get("ImaxRight") != null) {
                assertEquals(Double.parseDouble(expectedCnec.get("ImaxRight")), flowCnec.getIMax(TwoSides.TWO), DOUBLE_TOLERANCE);
            } else {
                assertNull(flowCnec.getIMax(TwoSides.TWO));
            }
            if (expectedCnec.get("NominalVoltageLeft") != null) {
                assertEquals(Double.parseDouble(expectedCnec.get("NominalVoltageLeft")), flowCnec.getNominalVoltage(TwoSides.ONE), DOUBLE_TOLERANCE);
            } else {
                assertNull(flowCnec.getNominalVoltage(TwoSides.ONE));
            }
            if (expectedCnec.get("NominalVoltageRight") != null) {
                assertEquals(Double.parseDouble(expectedCnec.get("NominalVoltageRight")), flowCnec.getNominalVoltage(TwoSides.TWO), DOUBLE_TOLERANCE);
            } else {
                assertNull(flowCnec.getNominalVoltage(TwoSides.TWO));
            }
        }
    }

    @Then("the flow cnecs should have the following thresholds:")
    public void theFlowCnecsShouldHaveTheFollowingThresholds(DataTable arg1) {
        List<Map<String, String>> expectedThresholds = arg1.asMaps(String.class, String.class);
        for (Map<String, String> expectedThreshold : expectedThresholds) {
            BranchCnec<?> branchCnec = crac.getFlowCnec(expectedThreshold.get("CnecId"));
            assertNotNull(branchCnec);
            Unit expectedUnit = Unit.valueOf(expectedThreshold.get("Unit"));
            TwoSides side = TwoSides.valueOf(expectedThreshold.get("Side"));
            Optional<Double> min = Optional.ofNullable(expectedThreshold.get("Min").equals("None") ? null : Double.parseDouble(expectedThreshold.get("Min")));
            Optional<Double> max = Optional.ofNullable(expectedThreshold.get("Max").equals("None") ? null : Double.parseDouble(expectedThreshold.get("Max")));
            assertTrue(branchCnec.getThresholds().stream().anyMatch(threshold -> matchThreshold(threshold, expectedUnit, side, min, max)));
        }
    }

    private boolean matchThreshold(BranchThreshold threshold, Unit unit, TwoSides side, Optional<Double> min, Optional<Double> max) {
        if (!unit.equals(threshold.getUnit())) {
            return false;
        }
        if (!side.equals(threshold.getSide())) {
            return false;
        }
        if (min.isPresent()) {
            if (threshold.min().isEmpty() || Math.abs(min.get() - threshold.min().get()) > DOUBLE_TOLERANCE) {
                return false;
            }
        } else {
            if (threshold.min().isPresent()) {
                return false;
            }
        }
        if (max.isPresent()) {
            return threshold.max().isPresent() && Math.abs(max.get() - threshold.max().get()) < DOUBLE_TOLERANCE;
        } else {
            return threshold.max().isEmpty();
        }
    }

    @Then("it should have the following angle CNECs:")
    public void itShouldHaveTheFollowingAngleCNECs(DataTable arg1) throws Exception {
        List<Map<String, String>> expectedCnecs = arg1.asMaps(String.class, String.class);
        assertEquals(expectedCnecs.size(), crac.getAngleCnecs().size());
        for (Map<String, String> expectedCnec : expectedCnecs) {
            String cnecId = expectedCnec.get("AngleCnecId");
            String cnecName = expectedCnec.get("Name");
            String importingElementId = expectedCnec.get("ImportingElementId");
            String exportingElementId = expectedCnec.get("ExportingElementId");
            String contingency = expectedCnec.get("Contingency");
            String instant = expectedCnec.get("Instant");
            boolean optimized = expectedCnec.get("Optimized").equalsIgnoreCase("yes");
            boolean monitored = expectedCnec.get("Monitored").equalsIgnoreCase("yes");

            Set<AngleCnec> cnecs = crac.getAngleCnecs(crac.getPreventiveState());
            cnecs.addAll(crac.getAngleCnecs(crac.getState(contingency, crac.getInstant(InstantKind.CURATIVE))));

            AngleCnec angleCnec = cnecs.stream()
                .filter(cnec -> cnec.getId()
                    .equals(cnecId))
                .filter(cnec -> cnec.getName()
                    .equals(cnecName))
                .filter(cnec -> cnec.getImportingNetworkElement().getId()
                    .equals(importingElementId))
                .filter(cnec -> cnec.getExportingNetworkElement().getId()
                    .equals(exportingElementId))
                .filter(cnec -> cnec.isOptimized() == optimized)
                .filter(cnec -> cnec.isMonitored() == monitored)
                .filter(cnec -> cnec.getState().getInstant().toString().equalsIgnoreCase(instant))
                .findFirst()
                .orElseThrow(Exception::new);

            Set<String> angleCnecsNetworkElementIds = angleCnec.getNetworkElements().stream().map(Identifiable::getId).collect(Collectors.toSet());
            assert angleCnecsNetworkElementIds.containsAll(Set.of(importingElementId, exportingElementId))
                && Set.of(importingElementId, exportingElementId).containsAll(angleCnecsNetworkElementIds);

            if (expectedCnec.get("LowerBound") != null) {
                Optional<Double> lowerBound = angleCnec.getLowerBound(Unit.DEGREE);
                if (lowerBound.isPresent()) {
                    assertEquals(Double.parseDouble(expectedCnec.get("LowerBound")), lowerBound.get(), DOUBLE_TOLERANCE);
                } else {
                    assertEquals("null", expectedCnec.get("LowerBound"));
                }
            }
            if (expectedCnec.get("UpperBound") != null) {
                Optional<Double> upperBound = angleCnec.getUpperBound(Unit.DEGREE);
                if (upperBound.isPresent()) {
                    assertEquals(Double.parseDouble(expectedCnec.get("UpperBound")), upperBound.get(), DOUBLE_TOLERANCE);
                } else {
                    assertEquals("null", expectedCnec.get("UpperBound"));
                }
            }
        }
    }

    @Then("it should have the following voltage CNECs:")
    public void itShouldHaveTheFollowingVoltageCNECs(DataTable arg1) {
        List<Map<String, String>> expectedCnecs = arg1.asMaps(String.class, String.class);
        assertEquals(expectedCnecs.size(), crac.getVoltageCnecs().size());
        for (Map<String, String> expectedCnec : expectedCnecs) {
            String cnecId = expectedCnec.get("VoltageCnecId");
            String networkElementId = expectedCnec.get("NetworkElementId");
            Instant instant = crac.getInstant(expectedCnec.get("Instant").toLowerCase());
            String contingency = expectedCnec.get("Contingency");
            String contingencyName = expectedCnec.get("ContingencyName");
            Double min = expectedCnec.get("Min").equals("null") ? null : Double.valueOf(expectedCnec.get("Min"));
            Double max = expectedCnec.get("Max").equals("null") ? null : Double.valueOf(expectedCnec.get("Max"));

            VoltageCnec voltageCnec = crac.getVoltageCnec(cnecId);
            assertEquals(networkElementId, voltageCnec.getNetworkElement().getId());
            if (instant.isPreventive()) {
                assertEquals(crac.getPreventiveState(), voltageCnec.getState());
                assertNull(contingencyName);
            } else {
                assertNotNull(contingencyName);
                assertEquals(crac.getState(contingency, instant), voltageCnec.getState());
                if (voltageCnec.getState().getContingency().isPresent()) {
                    assertEquals(Optional.of(contingencyName), voltageCnec.getState().getContingency().get().getName());
                } else {
                    throw new OpenRaoException("Contingency should be defined");
                }
            }
            assertEquals(1, voltageCnec.getThresholds().size());
            assertEquals(Optional.ofNullable(min), voltageCnec.getThresholds().iterator().next().min());
            assertEquals(Optional.ofNullable(max), voltageCnec.getThresholds().iterator().next().max());
        }
    }

    @Then("all the remedial actions should be imported successfully")
    public void allRasMustBeImported() {
        if (cracCreationContext == null) {
            throw new NotImplementedException(NOT_IMPLEMENTED_JSON);
        } else if (cracCreationContext instanceof UcteCracCreationContext ucteCracCreationContext) {
            assertTrue(ucteCracCreationContext.getRemedialActionCreationContexts().stream().allMatch(ElementaryCreationContext::isImported));
        } else {
            throw new NotImplementedException(String.format(TYPE_NOT_HANDLED, cracCreationContext.getClass().getName()));
        }
    }

    @Then("the native remedial action {string} should create the remedial action {string}")
    public void mapRemedialAction(String nativeRaId, String createdRaId) {
        if (cracCreationContext == null) {
            throw new NotImplementedException(NOT_IMPLEMENTED_JSON);
        } else if (cracCreationContext instanceof UcteCracCreationContext ucteCracCreationContext) {
            ElementaryCreationContext remedialActionCreationContext = ucteCracCreationContext.getRemedialActionCreationContext(nativeRaId);
            assertNotNull(remedialActionCreationContext);
            assertTrue(remedialActionCreationContext.isImported());
            assertEquals(createdRaId, remedialActionCreationContext.getCreatedObjectId());
        } else {
            throw new NotImplementedException(String.format(TYPE_NOT_HANDLED, cracCreationContext.getClass().getName()));
        }
    }

    @Then("the native remedial action {string} should not be imported because of {string}")
    public void raShouldNotBeImported(String nativeRaId, String importStatus) {
        if (cracCreationContext == null) {
            throw new NotImplementedException(NOT_IMPLEMENTED_JSON);
        } else if (cracCreationContext instanceof UcteCracCreationContext ucteCracCreationContext) {
            ElementaryCreationContext remedialActionCreationContext = ucteCracCreationContext.getRemedialActionCreationContext(nativeRaId);
            assertNotNull(remedialActionCreationContext);
            assertFalse(remedialActionCreationContext.isImported());
            assertNull(remedialActionCreationContext.getCreatedObjectId());
            assertEquals(ImportStatus.valueOf(importStatus), remedialActionCreationContext.getImportStatus());
        } else {
            throw new NotImplementedException(String.format(TYPE_NOT_HANDLED, cracCreationContext.getClass().getName()));
        }
    }

    @Then("it should have the following PST range actions:")
    public void itShouldHaveTheFollowingPstRangeActions(DataTable arg1) {
        List<Map<String, String>> expectedPsts = arg1.asMaps(String.class, String.class);

        int number = expectedPsts.stream().map(stringStringMap -> stringStringMap.get("PstRangeActionId")).collect(Collectors.toSet()).size();
        assertEquals(number, crac.getPstRangeActions().size());

        for (Map<String, String> expectedPst : expectedPsts) {
            String id = expectedPst.get("PstRangeActionId");
            PstRangeAction pstRangeAction = crac.getPstRangeAction(id);
            assertNotNull(pstRangeAction);
            assertEquals(expectedPst.get("PstRangeActionName"), pstRangeAction.getName());
            assertEquals(expectedPst.get("NetworkElementId"), pstRangeAction.getNetworkElement().getId());
            assertEquals(Integer.parseInt(expectedPst.get("InitialTap")), pstRangeAction.getInitialTap());
            int minTap = Collections.min(pstRangeAction.getTapToAngleConversionMap().keySet());
            int maxTap = Collections.max(pstRangeAction.getTapToAngleConversionMap().keySet());
            assertEquals(Integer.parseInt(expectedPst.get("MinTap")), minTap);
            assertEquals(Integer.parseInt(expectedPst.get("MaxTap")), maxTap);
            assertEquals(Double.parseDouble(expectedPst.get("MinTapAngle")), pstRangeAction.convertTapToAngle(minTap), DOUBLE_TOLERANCE);
            assertEquals(Double.parseDouble(expectedPst.get("MaxTapAngle")), pstRangeAction.convertTapToAngle(maxTap), DOUBLE_TOLERANCE);
        }
    }

    @Then("it should have the following injection range actions:")
    public void itShouldHaveTheFollowingInjectionRangeActions(DataTable arg1) {
        List<Map<String, String>> expectedRAs = arg1.asMaps(String.class, String.class);

        int number = expectedRAs.stream().map(stringStringMap -> stringStringMap.get("RangeActionId")).collect(Collectors.toSet()).size();
        assertEquals(number, crac.getInjectionRangeActions().size());

        for (Map<String, String> expectedRA : expectedRAs) {
            String id = expectedRA.get("RangeActionId");
            InjectionRangeAction rangeAction = crac.getInjectionRangeAction(id);
            assertNotNull(rangeAction);
            assertEquals(expectedRA.get("RangeActionName"), rangeAction.getName());
            String minRange = rangeAction.getRanges().stream().mapToDouble(StandardRange::getMin).max().isPresent() ? String.valueOf(rangeAction.getRanges().stream().mapToDouble(StandardRange::getMin).max().getAsDouble()) : "null";
            String maxRange = rangeAction.getRanges().stream().mapToDouble(StandardRange::getMax).min().isPresent() ? String.valueOf(rangeAction.getRanges().stream().mapToDouble(StandardRange::getMax).min().getAsDouble()) : "null";
            assertEquals(expectedRA.get("MaxRange"), maxRange);
            assertEquals(expectedRA.get("MinRange"), minRange);
            String groupId = rangeAction.getGroupId().isPresent() ? rangeAction.getGroupId().get() : "null";
            assertEquals(expectedRA.get("GroupId"), groupId);
        }
    }

    @Then("it should have the following HVDC range actions:")
    public void itShouldHaveTheFollowingHvdcRangeActions(DataTable arg1) {
        List<Map<String, String>> expectedRAs = arg1.asMaps(String.class, String.class);

        int number = expectedRAs.stream().map(stringStringMap -> stringStringMap.get("HvdcRangeActionId")).collect(Collectors.toSet()).size();
        assertEquals(number, crac.getHvdcRangeActions().size());

        for (Map<String, String> expectedRA : expectedRAs) {
            String id = expectedRA.get("HvdcRangeActionId");
            HvdcRangeAction rangeAction = crac.getHvdcRangeAction(id);
            assertNotNull(rangeAction);
            assertEquals(expectedRA.get("HvdcRangeActionName"), rangeAction.getName());
            assertEquals(expectedRA.get("NetworkElementId"), rangeAction.getNetworkElement().getId());
            String groupId = rangeAction.getGroupId().isPresent() ? rangeAction.getGroupId().get() : "null";
            assertEquals(expectedRA.get("GroupId"), groupId);
            assertEquals(Double.parseDouble(expectedRA.get("InitialSetpoint")), rangeAction.getInitialSetpoint(), DOUBLE_TOLERANCE);
        }
    }

    @Then("the PST range actions should have the following ranges:")
    public void thePstsShouldHaveRanges(DataTable arg1) {
        List<Map<String, String>> expectedPsts = arg1.asMaps(String.class, String.class);

        int number = expectedPsts.stream().map(stringStringMap -> stringStringMap.get("PstRangeActionId")).collect(Collectors.toSet()).size();
        assertEquals(number, crac.getPstRangeActions().size());

        for (Map<String, String> expectedPst : expectedPsts) {
            String id = expectedPst.get("PstRangeActionId");
            PstRangeAction pstRangeAction = crac.getPstRangeAction(id);
            assertNotNull(pstRangeAction);
            assertEquals(Integer.parseInt(expectedPst.get("Ranges")), pstRangeAction.getRanges().size());
            if (!pstRangeAction.getRanges().isEmpty()) {
                RangeType rangeType = RangeType.valueOf(expectedPst.get("RangeType"));
                assertTrue(pstRangeAction.getRanges().stream().anyMatch(range ->
                    range.getUnit().equals(Unit.TAP)
                        && range.getRangeType().equals(rangeType)
                        && range.getMinTap() == Integer.parseInt(expectedPst.get("MinTap"))
                        && range.getMaxTap() == Integer.parseInt(expectedPst.get("MaxTap"))
                ));
            }
        }
    }

    @Then("the PST {string} should have groupId {string}")
    public void thePstShouldHaveGroupId(String pstRangeActionId, String expectedGroupId) {
        RangeAction<?> rangeAction = CommonTestData.getCrac().getRangeAction(pstRangeActionId);
        assertNotNull(rangeAction);
        assertTrue(rangeAction.getGroupId().isPresent());
        assertEquals(expectedGroupId, rangeAction.getGroupId().get());
    }

    @Then("the HVDC range actions should have the following ranges:")
    public void theHvdcsShouldHaveRanges(DataTable arg1) {
        List<Map<String, String>> expectedHvdcs = arg1.asMaps(String.class, String.class);

        int number = expectedHvdcs.stream().map(stringStringMap -> stringStringMap.get("HvdcRangeActionId")).collect(Collectors.toSet()).size();
        assertEquals(number, crac.getHvdcRangeActions().size());

        for (Map<String, String> expectedHvdc : expectedHvdcs) {
            String id = expectedHvdc.get("HvdcRangeActionId");
            HvdcRangeAction hvdcRangeAction = crac.getHvdcRangeAction(id);
            assertNotNull(hvdcRangeAction);
            assertEquals(Integer.parseInt(expectedHvdc.get("Ranges")), hvdcRangeAction.getRanges().size());
            RangeType rangeType = RangeType.valueOf(expectedHvdc.get("RangeType"));
            assertTrue(hvdcRangeAction.getRanges().stream().anyMatch(range ->
                range.getUnit().equals(Unit.MEGAWATT)
                    && range.getRangeType().equals(rangeType)
                    && range.getMin() == Double.parseDouble(expectedHvdc.get("Min"))
                    && range.getMax() == Double.parseDouble(expectedHvdc.get("Max"))
            ));
        }
    }

    @Then("the injection range action {string} should have the following injection distribution keys:")
    public void itShouldHaveTheFollowingInjectionDistributionKeys(String injectionRangeActionId, DataTable arg2) {

        InjectionRangeAction ira = crac.getInjectionRangeAction(injectionRangeActionId);
        assertNotNull(ira);

        List<Map<String, String>> expectedInjectionAndKeys = arg2.asMaps(String.class, String.class);
        assertEquals(expectedInjectionAndKeys.size(), ira.getInjectionDistributionKeys().size());

        for (Map<String, String> expectedInjectionAndKey : expectedInjectionAndKeys) {
            String expectedInjectionId = expectedInjectionAndKey.get("InjectionId");
            String expectedKey = expectedInjectionAndKey.get("Key");

            Map.Entry<NetworkElement, Double> correspondingEntry = ira.getInjectionDistributionKeys().entrySet().stream()
                .filter(e -> e.getKey().getId().equals(expectedInjectionId))
                .findAny().orElse(null);

            assertNotNull(correspondingEntry);
            assertEquals(Double.parseDouble(expectedKey), correspondingEntry.getValue(), 1e-3);
        }
    }

    @Then("it should have the following network actions:")
    public void itShouldHaveTheFollowingNetworkActions(DataTable arg1) {
        List<Map<String, String>> expectedNetworkActions = arg1.asMaps(String.class, String.class);

        int number = expectedNetworkActions.stream().map(stringStringMap -> stringStringMap.get("NetworkActionId")).collect(Collectors.toSet()).size();
        assertEquals(number, crac.getNetworkActions().size());

        for (Map<String, String> expectedNetworkAction : expectedNetworkActions) {
            String id = expectedNetworkAction.get("NetworkActionId");
            NetworkAction networkAction = crac.getNetworkAction(id);
            assertNotNull(networkAction);
            assertEquals(expectedNetworkAction.get("NetworkActionName"), networkAction.getName());
            assertEquals(Integer.parseInt(expectedNetworkAction.get("ElementaryActions")), networkAction.getElementaryActions().size());
            String networkElementId = expectedNetworkAction.get("NetworkElementId");
            String action = expectedNetworkAction.get("Action/Setpoint");
            switch (expectedNetworkAction.get("ElementaryActionType")) {
                case "PhaseTapChangerTapPositionAction":
                    int tapPosition = Integer.parseInt(action);
                    assertTrue(networkAction.getElementaryActions().stream().anyMatch(elementaryAction ->
                        elementaryAction instanceof PhaseTapChangerTapPositionAction phaseTapChangerTapPositionAction
                            && Objects.equals(phaseTapChangerTapPositionAction.getTransformerId(), networkElementId)
                            && phaseTapChangerTapPositionAction.getTapPosition() == tapPosition));
                    break;
                case "TerminalsConnectionAction":
                    ActionType actionTypeTCA = ActionType.valueOf(action.toUpperCase());
                    assertTrue(networkAction.getElementaryActions().stream().anyMatch(elementaryAction ->
                        elementaryAction instanceof TerminalsConnectionAction terminalsConnectionAction
                            && Objects.equals(terminalsConnectionAction.getElementId(), networkElementId)
                            && terminalsConnectionAction.isOpen() == (actionTypeTCA == ActionType.OPEN)));
                    break;
                case "SwitchAction":
                    ActionType actionTypeSA = ActionType.valueOf(action.toUpperCase());
                    assertTrue(networkAction.getElementaryActions().stream().anyMatch(elementaryAction ->
                        elementaryAction instanceof SwitchAction switchAction
                            && Objects.equals(switchAction.getSwitchId(), networkElementId)
                            && switchAction.isOpen() == (actionTypeSA == ActionType.OPEN)));
                    break;
                case "GeneratorAction":
                    double activePowerValueGA = Double.parseDouble(action);
                    assertTrue(networkAction.getElementaryActions().stream().anyMatch(elementaryAction ->
                        elementaryAction instanceof GeneratorAction generatorAction
                            && Objects.equals(generatorAction.getGeneratorId(), networkElementId)
                            && generatorAction.getActivePowerValue().getAsDouble() == activePowerValueGA));
                    break;
                case "LoadAction":
                    double activePowerValueLA = Double.parseDouble(action);
                    assertTrue(networkAction.getElementaryActions().stream().anyMatch(elementaryAction ->
                        elementaryAction instanceof LoadAction loadAction
                            && Objects.equals(loadAction.getLoadId(), networkElementId)
                            && loadAction.getActivePowerValue().getAsDouble() == activePowerValueLA));
                    break;
                case "DanglingLineAction":
                    double activePowerValueDLA = Double.parseDouble(action);
                    assertTrue(networkAction.getElementaryActions().stream().anyMatch(elementaryAction ->
                        elementaryAction instanceof DanglingLineAction danglingLineAction
                            && Objects.equals(danglingLineAction.getDanglingLineId(), networkElementId)
                            && danglingLineAction.getActivePowerValue().getAsDouble() == activePowerValueDLA));
                    break;
                case "ShuntCompensatorPositionAction":
                    int sectionCount = Integer.parseInt(action);
                    assertTrue(networkAction.getElementaryActions().stream().anyMatch(elementaryAction ->
                        elementaryAction instanceof ShuntCompensatorPositionAction shuntCompensatorPositionAction
                            && Objects.equals(shuntCompensatorPositionAction.getShuntCompensatorId(), networkElementId)
                            && shuntCompensatorPositionAction.getSectionCount() == sectionCount));
                    break;
                case "SwitchPair":
                    assertEquals("OPEN/CLOSE", action);
                    String open = networkElementId.split("/")[0];
                    String close = networkElementId.split("/")[1];
                    assertTrue(networkAction.getElementaryActions().stream().anyMatch(elementaryAction ->
                        elementaryAction instanceof SwitchPair switchPair
                            && switchPair.getSwitchToOpen().getId().equals(open)
                            && switchPair.getSwitchToClose().getId().equals(close)));
                    break;
                default:
                    throw new IllegalArgumentException(String.format("Unknown elementary network action type: %s", expectedNetworkAction.get("ElementaryActionType")));
            }
        }
    }

    @Then("the remedial actions should have the following usage rules:")
    public void raShouldHaveUsageRules(DataTable arg1) {
        List<Map<String, String>> expectedUsageRules = arg1.asMaps(String.class, String.class);
        for (Map<String, String> expectedUsageRule : expectedUsageRules) {
            String raId = expectedUsageRule.get("RemedialActionId");
            RemedialAction<?> remedialAction = crac.getRemedialAction(raId);
            assertNotNull(remedialAction);
            int number = Integer.parseInt(expectedUsageRule.get("UsageRules"));
            assertEquals(number, remedialAction.getUsageRules().size());
            Instant instant = crac.getInstant(expectedUsageRule.get("Instant").toLowerCase());
            UsageMethod usageMethod = UsageMethod.valueOf(expectedUsageRule.get("Method").toUpperCase());
            switch (expectedUsageRule.get("Rule")) {
                case "OnInstant":
                    assertTrue(remedialAction.getUsageRules().stream().anyMatch(usageRule ->
                        usageRule instanceof OnInstant onInstant
                            && onInstant.getUsageMethod().equals(usageMethod)
                            && onInstant.getInstant().equals(instant)
                    ));
                    break;
                case "OnContingencyState":
                    Contingency contingency = crac.getContingency(expectedUsageRule.get("ContingencyId"));
                    assertNotNull(contingency);
                    assertTrue(remedialAction.getUsageRules().stream().anyMatch(usageRule ->
                        usageRule instanceof OnContingencyState onContingencyState
                            && onContingencyState.getUsageMethod().equals(usageMethod)
                            && onContingencyState.getInstant().equals(instant)
                            && onContingencyState.getContingency().equals(contingency)
                    ));
                    break;
                case "OnFlowConstraint":
                    FlowCnec flowCnec = crac.getFlowCnec(expectedUsageRule.get("FlowCnecId"));
                    assertNotNull(flowCnec);
                    assertTrue(remedialAction.getUsageRules().stream().anyMatch(usageRule ->
                        usageRule instanceof OnConstraint<?> onFlowConstraint
                            && onFlowConstraint.getUsageMethod().equals(usageMethod)
                            && onFlowConstraint.getInstant().equals(instant)
                            && onFlowConstraint.getCnec().equals(flowCnec)
                    ));
                    break;
                case "OnFlowConstraintInCountry":
                    Country country = Country.valueOf(expectedUsageRule.get("Country"));
                    Optional<Contingency> optionalContingency = Optional.ofNullable(crac.getContingency(expectedUsageRule.get("ContingencyId")));
                    assertTrue(remedialAction.getUsageRules().stream().anyMatch(usageRule ->
                        usageRule instanceof OnFlowConstraintInCountry onFlowConstraintInCountry
                            && onFlowConstraintInCountry.getUsageMethod().equals(usageMethod)
                            && onFlowConstraintInCountry.getInstant().equals(instant)
                            && onFlowConstraintInCountry.getCountry().equals(country)
                            && onFlowConstraintInCountry.getContingency().equals(optionalContingency)
                    ));
                    break;
                case "OnAngleConstraint":
                    AngleCnec angleCnec = crac.getAngleCnec(expectedUsageRule.get("AngleCnecId"));
                    assertNotNull(angleCnec);
                    assertTrue(remedialAction.getUsageRules().stream().anyMatch(usageRule ->
                        usageRule instanceof OnConstraint<?> onAngleConstraint
                            && onAngleConstraint.getUsageMethod().equals(usageMethod)
                            && onAngleConstraint.getInstant().equals(instant)
                            && onAngleConstraint.getCnec().equals(angleCnec)
                    ));
                    break;
                default:
                    throw new IllegalArgumentException(String.format("UsageRule unknown: %s", expectedUsageRule.get("Rule")));
            }
        }
    }

    @Then("it should have {int} cnecs")
    public void itShouldHaveCnecs(int expectedCnecNumber) {
        assertEquals(expectedCnecNumber, crac.getCnecs().size());
    }

    @Then("it should have {int} mnecs")
    public void itShouldHaveMnecs(int expectedMnecNumber) {
        assertEquals(expectedMnecNumber, crac.getCnecs().stream().filter(Cnec::isMonitored).count());
    }

    @Then("it should have {int} network actions")
    public void itShouldHaveNetworkActions(int expectedNetworkActions) {
        assertEquals(expectedNetworkActions, crac.getNetworkActions().size());
    }

    @Then("it should have {int} range actions")
    public void itShouldHaveRangeActions(int expectedRangeActions) {
        assertEquals(expectedRangeActions, crac.getRangeActions().size());
    }

    @Then("groupId for range action {string} should be {string}")
    public void rangeActionGroupIdShouldBe(String rangeActionId, String expectedGroupId) {
        assertNotNull(crac.getRangeAction(rangeActionId));
        if (expectedGroupId.equals("null")) {
            assertTrue(crac.getRangeAction(rangeActionId).getGroupId().isEmpty());
        } else {
            assertTrue(crac.getRangeAction(rangeActionId).getGroupId().isPresent());
            assertEquals(expectedGroupId, crac.getRangeAction(rangeActionId).getGroupId().get());
        }
    }

    @Then("range action {string} should have {int} ranges")
    public void rangeActionShouldHaveRanges(String rangeActionId, int expectedRanges) {
        assertNotNull(crac.getRangeAction(rangeActionId));
        assertEquals(expectedRanges, ((PstRangeAction) crac.getRangeAction(rangeActionId)).getRanges().size());
    }

}