AbstractMergeNetworkTest.java

/**
 * Copyright (c) 2018, 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/.
 * SPDX-License-Identifier: MPL-2.0
 */
package com.powsybl.iidm.network.tck;

import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.extensions.AbstractExtension;
import com.powsybl.commons.extensions.Extension;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.extensions.*;
import com.powsybl.iidm.network.test.EurostagTutorialExample1Factory;
import org.apache.commons.lang3.mutable.MutableBoolean;
import java.time.ZonedDateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.stream.Stream;

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

public abstract class AbstractMergeNetworkTest {

    private static final String MERGE = "merge";
    private static final String MERGE_DEFAULT_ID = "n1+n2";
    public static final String N1 = "n1";
    public static final String N2 = "n2";
    public static final String PROPERTY_NAME_1 = "property_name1";
    public static final String PROPERTY_NAME_2 = "property_name2";
    public static final String PROPERTY_NAME_3 = "property_name3";

    Network n0;
    Network n1;
    Network n2;

    @BeforeEach
    public void setup() {
        n0 = Network.create("a", "asdf");
        n1 = Network.create(N1, "asdf");
        n2 = Network.create(N2, "qwer");
    }

    @Test
    public void failMergeIfMultiVariants() {
        n1.getVariantManager().cloneVariant(VariantManagerConstants.INITIAL_VARIANT_ID, "Totest");
        PowsyblException e = assertThrows(PowsyblException.class, () -> Network.merge(n2, n1));
        assertTrue(e.getMessage().contains("Merging of multi-variants network is not supported"));
    }

    @Test
    public void failMergeWithSameObj() {
        addSubstation(n1, "P1");
        addSubstation(n2, "P1");
        PowsyblException e = assertThrows(PowsyblException.class, () -> Network.merge(n1, n2));
        assertEquals("The following object(s) of type SubstationImpl exist(s) in both networks: [P1]", e.getMessage());
    }

    @Test
    public void testMerge() {
        addCommonSubstationsAndVoltageLevels();
        Network n0 = Network.create("n0", "rid");
        addSubstationAndVoltageLevel(n0, "s0", Country.FR, "vl0", "b0");
        addCommonDanglingLines("dl1", "code", "dl2", "code");
        Network merge = Network.merge(n0, n1, n2);
        assertEquals(3, merge.getSubnetworks().size());
        assertEquals(1, merge.getSubnetwork(n0.getId()).getVoltageLevelCount());
        assertEquals(1, merge.getSubnetwork(N1).getVoltageLevelCount());
        assertEquals(1, merge.getSubnetwork(N2).getVoltageLevelCount());

        TieLine tieLine = merge.getTieLine("dl1 + dl2");
        assertNotNull(tieLine);
        assertEquals("dl1_name + dl2_name", tieLine.getOptionalName().orElse(null));
        assertEquals("dl1_name + dl2_name", tieLine.getNameOrId());

        Network subnetwork1 = merge.getSubnetwork(N1);
        Network subnetwork2 = merge.getSubnetwork(N2);
        checkDanglingLineStatusCount(merge, 0, 2);
        checkDanglingLineStatusCount(subnetwork1, 0, 1);
        checkDanglingLineStatusCount(subnetwork2, 0, 1);

        assertEquals(merge, tieLine.getParentNetwork());
        assertEquals(subnetwork1, merge.getDanglingLine("dl1").getParentNetwork());
        assertEquals(subnetwork2, merge.getDanglingLine("dl2").getParentNetwork());
    }

    @Test
    public void testMergeAndDetach() {
        addCommonSubstationsAndVoltageLevels();
        addCommonDanglingLines("dl1", "code", "dl2", "code");
        // merge(n1, n2)
        Network merge = Network.merge(MERGE, n1, n2);
        TieLine tieLine = merge.getTieLine("dl1 + dl2");
        assertNotNull(tieLine);
        assertEquals("dl1_name + dl2_name", tieLine.getOptionalName().orElse(null));
        assertEquals("dl1_name + dl2_name", tieLine.getNameOrId());
        assertEquals("dl1", tieLine.getDanglingLine1().getId());
        assertEquals("dl2", tieLine.getDanglingLine2().getId());
        assertEquals(0.0, tieLine.getDanglingLine1().getP0());
        assertEquals(0.0, tieLine.getDanglingLine1().getQ0());
        assertEquals(0.0, tieLine.getDanglingLine2().getP0());
        assertEquals(0.0, tieLine.getDanglingLine2().getQ0());

        Network subnetwork1 = merge.getSubnetwork(N1);
        Network subnetwork2 = merge.getSubnetwork(N2);
        checkDanglingLineStatusCount(merge, 0, 2);
        checkDanglingLineStatusCount(subnetwork1, 0, 1);
        checkDanglingLineStatusCount(subnetwork2, 0, 1);
        checkSubstationAndVoltageLevelCounts(merge, 2, 2);

        assertEquals(merge, tieLine.getParentNetwork());
        assertEquals(subnetwork1, merge.getDanglingLine("dl1").getParentNetwork());
        assertEquals(subnetwork2, merge.getDanglingLine("dl2").getParentNetwork());

        // detach(n1)
        assertTrue(subnetwork1.isDetachable());
        Network detachedN1 = subnetwork1.detach();
        checkDanglingLineStatusCount(merge, 1, 0);
        checkDanglingLineStatusCount(detachedN1, 1, 0);
        checkDanglingLineStatusCount(subnetwork2, 1, 0);
        checkSubstationAndVoltageLevelCounts(merge, 1, 1);
        checkSubstationAndVoltageLevelCounts(detachedN1, 1, 1);
        DanglingLine dl1 = detachedN1.getDanglingLine("dl1");
        DanglingLine dl2 = merge.getDanglingLine("dl2");
        // - P0 and Q0 of the removed tie line's underlying dangling lines were updated:
        assertEquals(-731.312, dl1.getP0(), 0.001);
        assertEquals(-1254.625, dl1.getQ0(), 0.001);
        assertEquals(-731.312, dl2.getP0(), 0.001);
        assertEquals(-1254.625, dl2.getQ0(), 0.001);

        // detach(n2)
        assertTrue(subnetwork2.isDetachable());
        Network detachedN2 = subnetwork2.detach();
        checkDanglingLineStatusCount(merge, 0, 0);
        checkDanglingLineStatusCount(detachedN1, 1, 0);
        checkDanglingLineStatusCount(detachedN2, 1, 0);
        checkSubstationAndVoltageLevelCounts(merge, 0, 0);
        checkSubstationAndVoltageLevelCounts(detachedN1, 1, 1);
        checkSubstationAndVoltageLevelCounts(detachedN2, 1, 1);
    }

    private void checkSubstationAndVoltageLevelCounts(Network n, long substationCount, long voltageLevelCount) {
        assertEquals(substationCount, n.getSubstationCount());
        assertEquals(voltageLevelCount, n.getVoltageLevelCount());
    }

    @Test
    public void testMergeAndFlatten() {
        addCommonSubstationsAndVoltageLevels();
        addCommonDanglingLines("dl1", "code", "dl2", "code");
        // merge(n1, n2)
        Network network = Network.merge(MERGE, n1, n2);
        Network subnetwork1 = network.getSubnetwork(N1);
        Network subnetwork2 = network.getSubnetwork(N2);

        // flatten
        network.flatten();
        checkDanglingLineStatusCount(network, 0, 2);
        checkSubstationAndVoltageLevelCounts(network, 2, 2);
        checkSubstationAndVoltageLevelsNetworks(network, network, network);
        assertTrue(network.getSubnetworks().isEmpty());
        checkDanglingLineStatusCount(subnetwork1, 0, 0);
        checkDanglingLineStatusCount(subnetwork2, 0, 0);
        checkSubstationAndVoltageLevelCounts(subnetwork1, 0, 0);
        checkSubstationAndVoltageLevelCounts(subnetwork2, 0, 0);

        // Use the flatten network in another merge
        Network network2 = Network.merge(network, Network.create("n3", "manual"));
        Network mergedAsSubnetwork = network2.getSubnetwork("merge");
        checkSubstationAndVoltageLevelCounts(network2, 2, 2);
        checkSubstationAndVoltageLevelsNetworks(network2, network2, mergedAsSubnetwork);

        // And even if detached, everything is alright
        Network detachedN2 = mergedAsSubnetwork.detach();
        checkSubstationAndVoltageLevelCounts(detachedN2, 2, 2);
        checkSubstationAndVoltageLevelsNetworks(detachedN2, detachedN2, detachedN2);
    }

    private void checkSubstationAndVoltageLevelsNetworks(Network network, Network expectedNetwork, Network expectedParentNetwork) {
        Stream.concat(network.getSubstationStream().map(Identifiable.class::cast),
                        network.getVoltageLevelStream().map(Identifiable.class::cast))
                .forEach(substation -> {
                    assertEquals(expectedNetwork, substation.getNetwork());
                    assertEquals(expectedParentNetwork, substation.getParentNetwork());
                });
    }

    @Test
    public void testMergeAndDetachWithProperties() {
        // Add properties to the networks
        n1.setProperty(PROPERTY_NAME_1, "property_value1");
        n2.setProperty(PROPERTY_NAME_2, "property_value2");

        // Merge the networks and check that the properties have been transferred to subnetworks
        Network merged = Network.merge(n1, n2);
        assertFalse(merged.hasProperty());
        assertEquals("property_value1", merged.getSubnetwork(N1).getProperty(PROPERTY_NAME_1));
        assertEquals("property_value2", merged.getSubnetwork(N2).getProperty(PROPERTY_NAME_2));

        // Detach the subnetworks and check that the properties have been transferred to the detached networks
        Network detached1 = merged.getSubnetwork(N1).detach();
        Network detached2 = merged.getSubnetwork(N2).detach();
        assertEquals("property_value1", detached1.getProperty(PROPERTY_NAME_1));
        assertEquals("property_value2", detached2.getProperty(PROPERTY_NAME_2));
    }

    @Test
    public void testMergeAndFlattenWithProperties() {
        n1.setProperty(PROPERTY_NAME_1, "n1-val1");
        n1.setProperty(PROPERTY_NAME_2, "n1-val2");
        n2.setProperty(PROPERTY_NAME_1, "n2-val1");
        n2.setProperty(PROPERTY_NAME_3, "n2-val3");
        // merge(n1, n2)
        Network network = Network.merge(MERGE, n1, n2);
        Network subnetwork1 = network.getSubnetwork(N1);
        Network subnetwork2 = network.getSubnetwork(N2);
        network.setProperty("property_name_4", "val");

        // flatten
        network.flatten();
        // The 1st property is present in both subnetworks.
        // The value of the 1st one (in the merging order) is set in the flatten network and
        // the property is kept in subnetwork2. This way, it is possible to detect duplicates and
        // to handle them manually if wanted.
        assertEquals("n1-val1", network.getProperty(PROPERTY_NAME_1));
        assertEquals("n1-val2", network.getProperty(PROPERTY_NAME_2));
        assertEquals("n2-val3", network.getProperty(PROPERTY_NAME_3));
        assertEquals("val", network.getProperty("property_name_4"));
        assertTrue(subnetwork1.getPropertyNames().isEmpty());
        assertEquals(1, subnetwork2.getPropertyNames().size());
        assertEquals(PROPERTY_NAME_1, subnetwork2.getPropertyNames().stream().findFirst().orElseThrow());
    }

    @Test
    public void testMergeAndDetachWithExtensions() {
        n1 = EurostagTutorialExample1Factory.createWithMoreGenerators();
        addSubstationAndVoltageLevel(n2, "s2", Country.BE, "vl2", "b2");
        addDanglingLines(n1, "VLGEN", "dl1", "code", "NGEN");
        addDanglingLines(n2, "vl2", "dl2", "code", "b2");

        // Add extension at network level
        n1.newExtension(SecondaryVoltageControlAdder.class)
                .newControlZone()
                    .withName("z1")
                    .newPilotPoint()
                        .withBusbarSectionsOrBusesIds(List.of("NLOAD"))
                        .withTargetV(15d)
                    .add()
                    .newControlUnit()
                        .withId("GEN")
                        .withParticipate(false)
                    .add()
                    .newControlUnit()
                        .withId("GEN2")
                        .add()
                    .add()
                .add();
        // Add extension at inner element level
        n1.getLoad("LOAD").newExtension(LoadDetailAdder.class)
                .withFixedActivePower(40f)
                .withFixedReactivePower(20f)
                .withVariableActivePower(60f)
                .withVariableReactivePower(30f)
                .add();

        Network merge = Network.merge(MERGE, n1, n2);
        Network subnetwork1 = merge.getSubnetwork("sim1");
        checkExtensions(subnetwork1);

        Network detachedN1 = subnetwork1.detach();
        checkExtensions(detachedN1);
    }

    private static void checkExtensions(Network network) {
        // Check that the Network extension is present on the given network
        assertEquals(1, network.getExtensions().size());
        assertNotNull(network.getExtensionByName(SecondaryVoltageControl.NAME));
        assertNotNull(network.getExtension(SecondaryVoltageControl.class));

        // Check that the Load extension is visible from the given network
        assertEquals(1, network.getLoad("LOAD").getExtensions().size());
        assertNotNull(network.getLoad("LOAD").getExtensionByName(LoadDetail.NAME));
        assertNotNull(network.getLoad("LOAD").getExtension(LoadDetail.class));
    }

    @Test
    public void testMergeAndFlattenWithExtensions() {
        n1.addExtension(FooNetworkExtension.class, new FooNetworkExtensionImpl(N1));
        n2.addExtension(FooNetworkExtension.class, new FooNetworkExtensionImpl(N2));
        n2.addExtension(BarNetworkExtension.class, new BarNetworkExtensionImpl());

        Network merge = Network.merge(MERGE, n1, n2);
        Network subnetwork1 = merge.getSubnetwork(N1);
        Network subnetwork2 = merge.getSubnetwork(N2);
        assertEquals(0, merge.getExtensions().size());
        assertEquals(1, subnetwork1.getExtensions().size());
        assertEquals(2, subnetwork2.getExtensions().size());

        merge.flatten();
        // A FooNetworkExtension extension is present in both subnetworks.
        // The 1st one (in the merging order) is transferred to the flatten network and
        // the extension is kept in subnetwork2. This way, it is possible to detect duplicates and
        // to handle them manually if wanted.
        assertEquals(2, merge.getExtensions().size());
        assertEquals(0, subnetwork1.getExtensions().size());
        assertEquals(1, subnetwork2.getExtensions().size());
        Extension<Network> extension = subnetwork2.getExtensions().stream().findFirst().orElseThrow();
        assertInstanceOf(FooNetworkExtension.class, extension);
        assertEquals(N2, ((FooNetworkExtension) extension).getVal());
    }

    interface FooNetworkExtension {
        String getVal();
    }

    static class FooNetworkExtensionImpl extends AbstractExtension<Network> implements FooNetworkExtension {
        private final String val;

        public FooNetworkExtensionImpl(String val) {
            this.val = val;
        }

        @Override
        public String getName() {
            return "Foo";
        }

        @Override
        public String getVal() {
            return val;
        }
    }

    interface BarNetworkExtension {
    }

    static class BarNetworkExtensionImpl extends AbstractExtension<Network> implements BarNetworkExtension {
        @Override
        public String getName() {
            return "Bar";
        }
    }

    @Test
    public void failFlattenSubnetwork() {
        Network merged = Network.merge(n1, n2);
        Network subnetwork1 = merged.getSubnetwork(N1);
        Exception e = assertThrows(UnsupportedOperationException.class, subnetwork1::flatten);
        assertEquals("Subnetworks cannot be flattened.", e.getMessage());
    }

    @Test
    public void failDetachWithALineBetween2Subnetworks() {
        addCommonSubstationsAndVoltageLevels();
        Network merge = Network.merge(MERGE, n1, n2);
        merge.newLine()
                .setId("line1")
                .setVoltageLevel1("vl1")
                .setVoltageLevel2("vl2")
                .setBus1("b1")
                .setBus2("b2")
                .setR(1)
                .setX(1)
                .setG1(0)
                .setB1(0)
                .setG2(0)
                .setB2(0)
                .add();
        Network subnetwork1 = merge.getSubnetwork(N1);
        assertFalse(subnetwork1.isDetachable());
        PowsyblException e = assertThrows(PowsyblException.class, subnetwork1::detach);
        assertEquals("Un-splittable boundary elements prevent the subnetwork to be detached: line1", e.getMessage());
    }

    @Test
    public void failDetachIfMultiVariants() {
        addCommonSubstationsAndVoltageLevels();
        Network merge = Network.merge(MERGE, n1, n2);
        merge.getVariantManager().cloneVariant(VariantManagerConstants.INITIAL_VARIANT_ID, "Totest");

        Network subnetwork1 = merge.getSubnetwork(N1);
        assertFalse(subnetwork1.isDetachable());
        PowsyblException e = assertThrows(PowsyblException.class, subnetwork1::detach);
        assertTrue(e.getMessage().contains("Detaching from multi-variants network is not supported"));
    }

    @Test
    public void testMerge3Networks() {
        addCommonSubstationsAndVoltageLevels();
        addCommonDanglingLines("dl1", "code", "dl2", "code");
        Network n3 = Network.create("n3", "test");
        addSubstationAndVoltageLevel(n3, "s3", Country.DE, "vl3", "b3");
        addDanglingLines(n1, "vl1", "dl3", "code2", "b1");
        addDanglingLines(n3, "vl3", "dl4", "code2", "b3");

        //merge.merge(n1, n2, n3);
        Network merge = Network.merge(MERGE, n1, n2, n3);
        TieLine tieLine1 = merge.getTieLine("dl1 + dl2");
        assertNotNull(tieLine1);
        assertEquals("dl1_name + dl2_name", tieLine1.getOptionalName().orElse(null));
        assertEquals("dl1_name + dl2_name", tieLine1.getNameOrId());
        TieLine tieLine2 = merge.getTieLine("dl3 + dl4");
        assertNotNull(tieLine2);
        assertEquals("dl3_name + dl4_name", tieLine2.getOptionalName().orElse(null));
        assertEquals("dl3_name + dl4_name", tieLine2.getNameOrId());

        Network subnetwork1 = merge.getSubnetwork(N1);
        Network subnetwork2 = merge.getSubnetwork(N2);
        Network subnetwork3 = merge.getSubnetwork("n3");
        checkDanglingLineStatusCount(merge, 0, 4);
        checkDanglingLineStatusCount(subnetwork1, 0, 2);
        checkDanglingLineStatusCount(subnetwork2, 0, 1);
        checkDanglingLineStatusCount(subnetwork3, 0, 1);

        assertEquals(merge, tieLine1.getParentNetwork());
        assertEquals(merge, tieLine2.getParentNetwork());
        assertEquals(subnetwork1, merge.getDanglingLine("dl1").getParentNetwork());
        assertEquals(subnetwork1, merge.getDanglingLine("dl3").getParentNetwork());
        assertEquals(subnetwork2, merge.getDanglingLine("dl2").getParentNetwork());
        assertEquals(subnetwork3, merge.getDanglingLine("dl4").getParentNetwork());
    }

    private void checkDanglingLineStatusCount(Network network, long unpairedNb, long pairedNb) {
        assertEquals(pairedNb, network.getDanglingLineStream(DanglingLineFilter.PAIRED).count());
        assertEquals(unpairedNb, network.getDanglingLineStream(DanglingLineFilter.UNPAIRED).count());
    }

    @Test
    public void failMergeDanglingLinesWithSameId() {
        addCommonSubstationsAndVoltageLevels();
        addCommonDanglingLines("dl", null, "dl", "code");
        PowsyblException e = assertThrows(PowsyblException.class, () -> Network.merge(n0, n1, n2));
        assertTrue(e.getMessage().contains("The following object(s) of type DanglingLineImpl exist(s) in both networks: [dl]"));
    }

    @Test
    public void testValidationLevelWhenMerging2Eq() {
        addCommonSubstationsAndVoltageLevels();
        n1.setMinimumAcceptableValidationLevel(ValidationLevel.EQUIPMENT);
        n1.getVoltageLevel("vl1").newLoad()
                .setId("unchecked1")
                .setBus("b1")
                .setConnectableBus("b1")
                .add();
        n2.setMinimumAcceptableValidationLevel(ValidationLevel.EQUIPMENT);
        n2.getVoltageLevel("vl2").newLoad()
                .setId("unchecked2")
                .setBus("b2")
                .setConnectableBus("b2")
                .add();

        // merge(n1, n2)
        Network merge = Network.merge(MERGE, n1, n2);

        assertValidationLevels(merge, ValidationLevel.EQUIPMENT);
    }

    @Test
    public void testValidationLevelWhenMergingEqAndSsh() {
        addCommonSubstationsAndVoltageLevels();
        n1.getVoltageLevel("vl1").newLoad()
                .setId("unchecked1")
                .setBus("b1")
                .setConnectableBus("b1")
                .setP0(1.0).setQ0(1.0)
                .add();
        n2.setMinimumAcceptableValidationLevel(ValidationLevel.EQUIPMENT);
        n2.getVoltageLevel("vl2").newLoad()
                .setId("unchecked2")
                .setBus("b2")
                .setConnectableBus("b2")
                .add();

        // merge(n1, n2)
        Network merge = Network.merge(MERGE, n1, n2);

        assertValidationLevels(merge, ValidationLevel.EQUIPMENT);
    }

    @Test
    public void testValidationLevelWhenMerging2Ssh() {
        addCommonSubstationsAndVoltageLevels();
        n1.getVoltageLevel("vl1").newLoad()
                .setId("unchecked1")
                .setBus("b1")
                .setConnectableBus("b1")
                .setP0(1.0).setQ0(1.0)
                .add();
        n2.getVoltageLevel("vl2").newLoad()
                .setId("unchecked2")
                .setBus("b2")
                .setConnectableBus("b2")
                .setP0(1.0).setQ0(1.0)
                .add();

        // merge(n1, n2)
        Network merge = Network.merge(MERGE, n1, n2);

        assertValidationLevels(merge, ValidationLevel.STEADY_STATE_HYPOTHESIS);
    }

    void assertValidationLevels(Network merge, ValidationLevel expected) {
        // The validation level must be the same between the root network and its subnetworks
        assertEquals(expected, merge.getValidationLevel());
        assertEquals(expected, merge.getSubnetwork(N1).getValidationLevel());
        assertEquals(expected, merge.getSubnetwork(N2).getValidationLevel());
    }

    @Test
    public void failMergeOnlyOneNetwork() {
        Exception e = assertThrows(IllegalArgumentException.class, () -> Network.merge(MERGE, n1));
        assertTrue(e.getMessage().contains("At least 2 networks are expected"));
    }

    @Test
    public void failMergeOnSubnetworks() {
        Network merge = Network.merge(MERGE, n1, n2);
        Network subnetwork1 = merge.getSubnetwork(N1);
        Network other1 = Network.create("other1", "format");
        Network other2 = Network.create("other2", "format");

        Exception e = assertThrows(IllegalArgumentException.class, () -> Network.merge(subnetwork1, other1));
        assertEquals("The network n1 is already a subnetwork", e.getMessage());

        e = assertThrows(IllegalArgumentException.class, () -> Network.merge(subnetwork1, other1, other2));
        assertEquals("The network n1 is already a subnetwork", e.getMessage());
    }

    @Test
    public void failMergeSubnetworks() {
        Network merge = Network.merge(MERGE, n1, n2);
        Network subnetwork1 = merge.getSubnetwork(N1);
        Network other = Network.create("other", "format");

        Exception e = assertThrows(IllegalArgumentException.class,
                () -> Network.merge("test", other, subnetwork1));
        assertTrue(e.getMessage().contains("is already a subnetwork"));
    }

    @Test
    public void failMergeContainingSubnetworks() {
        Network merge = Network.merge(MERGE, n1, n2);
        Network other = Network.create("other", "format");

        Exception e = assertThrows(IllegalArgumentException.class,
                () -> Network.merge("test", other, merge));
        assertTrue(e.getMessage().contains("already contains subnetworks"));
    }

    @Test
    public void testMergeAndDetachWithDistinctAreas() {
        addCommonSubstationsAndVoltageLevels();
        Area n1TsoA = addArea(n1, "tsoA", "tso");
        Area n2BzB = n2.newArea().setId("bzB").setName("bzB_Name").setAreaType("bz").setFictitious(true).setInterchangeTarget(100.).add();
        n1TsoA.addVoltageLevel(n1.getVoltageLevel("vl1"));
        n2BzB.addVoltageLevel(n2.getVoltageLevel("vl2"));

        // Merge
        Network merged = Network.merge(n1, n2);
        checkAreas(merged, List.of("tsoA", "bzB"));
        checkAreaTypes(merged, List.of("tso", "bz"));
        final Area mergedBzB = merged.getArea("bzB");
        assertNotNull(mergedBzB);
        assertTrue(mergedBzB.isFictitious());
        assertTrue(mergedBzB.getInterchangeTarget().isPresent());
        assertEquals(100., mergedBzB.getInterchangeTarget().getAsDouble());

        // Detach n1, and check its content
        Network n1Detached = merged.getSubnetwork(n1.getId()).detach();
        checkAreas(n1Detached, List.of("tsoA"));
        checkAreaTypes(n1Detached, List.of("tso"));
        // Detached elements were removed from merged network
        checkAreas(merged, List.of("bzB"));
        checkAreaTypes(merged, List.of("bz"));

        // Detach n2, and check its content
        Network n2Detached = merged.getSubnetwork(n2.getId()).detach();
        checkAreas(n2Detached, List.of("bzB"));
        checkAreaTypes(n2Detached, List.of("bz"));
        final Area detachedBzB = n2Detached.getArea("bzB");
        assertNotNull(detachedBzB);
        assertTrue(detachedBzB.isFictitious());
        assertTrue(detachedBzB.getInterchangeTarget().isPresent());
        assertEquals(100., detachedBzB.getInterchangeTarget().getAsDouble());
    }

    @Test
    public void testNoEmptyAdditionalSubnetworkIsCreated() {
        Network merge = Network.merge(MERGE, n1, n2);
        assertEquals(2, merge.getSubnetworks().size());
        assertNull(merge.getSubnetwork(MERGE));
        assertNotNull(merge.getSubnetwork(N1));
        assertNotNull(merge.getSubnetwork(N2));
    }

    private static void checkAreas(final Network merged, final List<String> expectedAreaIds) {
        assertEquals(expectedAreaIds.size(), merged.getAreaStream().count());
        assertTrue(merged.getAreaStream().map(Area::getId).toList().containsAll(expectedAreaIds));
        final List<String> expectedNames = expectedAreaIds.stream().map(id -> id + "_Name").toList();
        assertTrue(merged.getAreaStream().map(Area::getNameOrId).toList().containsAll(expectedNames));
    }

    private static void checkAreaTypes(final Network merged, final List<String> expectedAreaTypeIds) {
        assertEquals(expectedAreaTypeIds.size(), merged.getAreaTypeStream().count());
        assertTrue(merged.getAreaTypeStream().toList().containsAll(expectedAreaTypeIds));
    }

    @Test
    public void failMergeWithCommonAreaConflict() {
        addArea(n1, "bza", "bz");
        addArea(n2, "bza", "tso");

        // Merge should fail, because area "bza" is present in both network but with different attribute values
        PowsyblException e = assertThrows(PowsyblException.class, () -> Network.merge(n1, n2));
        assertEquals("The following object(s) of type AreaImpl exist(s) in both networks: [bza]", e.getMessage());
    }

    @Test
    public void testListeners() {
        MutableBoolean listenerCalled = new MutableBoolean(false);
        NetworkListener listener = new DefaultNetworkListener() {
            @Override
            public void onCreation(Identifiable identifiable) {
                listenerCalled.setTrue();
            }
        };

        // The listener works on n1.
        n1.addListener(listener);
        addSubstation(n1, "s1");
        assertTrue(listenerCalled.booleanValue());

        Network merge = Network.merge(MERGE, n1, n2);
        Network subnetwork1 = merge.getSubnetwork(N1);
        Network subnetwork2 = merge.getSubnetwork(N2);

        // After the merge, changes on "merge" or on "subnetwork1" are not reported to the listener.
        listenerCalled.setFalse();
        addSubstation(merge, "s2");
        assertFalse(listenerCalled.booleanValue());
        addSubstation(subnetwork1, "s3");
        assertFalse(listenerCalled.booleanValue());

        // Add the listener to "merge". Changes on subnetwork1 are reported.
        merge.addListener(listener);
        addSubstation(subnetwork1, "s4");
        assertTrue(listenerCalled.booleanValue());

        // Detach "subnetwork1". Changes on the new Network aren't reported to the listener.
        Network n = subnetwork1.detach();
        listenerCalled.setFalse();
        addSubstation(n, "s5");
        assertFalse(listenerCalled.booleanValue());

        // Changes on "merge" or "subnetwork2" are still reported to the listener.
        addSubstation(merge, "s6");
        assertTrue(listenerCalled.booleanValue());
        listenerCalled.setFalse();
        addSubstation(subnetwork2, "s7");
        assertTrue(listenerCalled.booleanValue());
    }

    private static Area addArea(Network network, String id, String areaType) {
        return network.newArea()
                      .setId(id)
                      .setName(id + "_Name")
                      .setAreaType(areaType)
                      .add();
    }

    private void addSubstation(Network network, String substationId) {
        network.newSubstation()
                            .setId(substationId)
                            .setCountry(Country.FR)
                            .setTso("RTE")
                            .setGeographicalTags("A")
                        .add();
    }

    private void addCommonSubstationsAndVoltageLevels() {
        addSubstationAndVoltageLevel(n1, "s1", Country.FR, "vl1", "b1");
        addSubstationAndVoltageLevel(n2, "s2", Country.BE, "vl2", "b2");
    }

    private void addSubstationAndVoltageLevel(Network network, String substationId, Country country, String vlId, String busId) {
        Substation s = network.newSubstation()
                .setId(substationId)
                .setCountry(country)
                .add();
        addVoltageLevel(s.newVoltageLevel(), vlId, 380, busId);
    }

    private static void addVoltageLevel(VoltageLevelAdder s, String vlId, int nominalV, String busId) {
        VoltageLevel vl = s
                .setId(vlId)
                .setNominalV(nominalV)
                .setTopologyKind(TopologyKind.BUS_BREAKER)
                .add();
        vl.getBusBreakerView().newBus()
                .setId(busId)
                .add();
    }

    private void addCommonDanglingLines(String dl1, String code1, String dl2, String code2) {
        addDanglingLines(n1, "vl1", dl1, code1, "b1");
        addDanglingLines(n2, "vl2", dl2, code2, "b2");
    }

    private void addDanglingLines(Network network, String voltageLevelId, String dlId, String code, String busId) {
        addDanglingLine(network, voltageLevelId, dlId, code, busId, busId);
    }

    private static void addDanglingLine(Network n, String voltageLevelId, String id, String code, String connectableBus, String bus) {
        DanglingLine dl = n.getVoltageLevel(voltageLevelId).newDanglingLine()
                .setId(id)
                .setName(id + "_name")
                .setConnectableBus(connectableBus)
                .setBus(bus)
                .setP0(0.0)
                .setQ0(0.0)
                .setR(1.0)
                .setX(2.0)
                .setG(4.0)
                .setB(5.0)
                .setPairingKey(code)
                .add();
        Terminal t = dl.getTerminal();
        t.setP(1.);
        t.setQ(2.);
        Bus b = t.getBusView().getBus();
        if (b != null) {
            b.setAngle(3.);
            b.setV(4.);
        }
    }

    @Test
    public void test() {
        ZonedDateTime d1 = n1.getCaseDate();
        ZonedDateTime d2 = n2.getCaseDate();
        addCommonSubstationsAndVoltageLevels();
        addLoad(n1, 1);
        addLoad(n2, 2);
        Network merge = Network.merge(MERGE, n0, n1, n2);
        assertEquals(MERGE, merge.getId());
        assertEquals("hybrid", merge.getSourceFormat());
        assertEquals(3, merge.getSubnetworks().size());
        checks(merge, 1, "asdf", d1);
        checks(merge, 2, "qwer", d2);

        // Parent network should remain indexed with the same id
        Identifiable<?> m = merge.getIdentifiable(MERGE);
        assertEquals(m, merge);
        // Subnetwork with elements shall keep its elements
        Network m1 = merge.getSubnetwork(N1);
        assertNotNull(m1);
        assertEquals(1, m1.getSubstationCount());
        assertEquals(1, m1.getVoltageLevelCount());
        // Substation and voltage level created on subnetwork shall be affected to subnetwork
        merge.getSubnetwork(N1).newSubstation().setId("s1bis").add().newVoltageLevel().setId("vl1bis").setTopologyKind(TopologyKind.BUS_BREAKER).setNominalV(90.0).add();
        Substation s1bis = merge.getSubstation("s1bis");
        assertNotNull(s1bis);
        assertSame(s1bis, merge.getSubnetwork(N1).getSubstation("s1bis"));
        assertSame(merge.getSubnetwork(N1), s1bis.getParentNetwork());
        assertNull(merge.getSubnetwork(N2).getSubstation("s1bis"));
        VoltageLevel vl1bis = merge.getVoltageLevel("vl1bis");
        assertNotNull(vl1bis);
        assertSame(vl1bis, merge.getSubnetwork(N1).getVoltageLevel("vl1bis"));
        assertSame(merge.getSubnetwork(N1), vl1bis.getParentNetwork());
        assertNull(merge.getSubnetwork(N2).getVoltageLevel("vl1bis"));
        // Voltage level created on subnetwork shall be affected to subnetwork
        merge.getSubnetwork(N2).newVoltageLevel().setId("vl2bis").setTopologyKind(TopologyKind.BUS_BREAKER).setNominalV(90.0).add();
        VoltageLevel vl2bis = merge.getVoltageLevel("vl2bis");
        assertNotNull(vl2bis);
        assertSame(vl2bis, merge.getSubnetwork(N2).getVoltageLevel("vl2bis"));
        assertSame(merge.getSubnetwork(N2), vl2bis.getParentNetwork());
        assertNull(merge.getSubnetwork(N1).getVoltageLevel("vl2bis"));
    }

    private static void addLoad(Network n, int num) {
        n.getVoltageLevel("vl" + num).newLoad()
                .setId("l" + num)
                .setBus("b" + num)
                .setP0(0.0)
                .setQ0(0.0)
                .add();
    }

    private static void checks(Network merge, int num, String sourceFormat, ZonedDateTime d) {
        Network n = merge.getSubnetwork("n" + num);
        assertNotNull(n);
        assertEquals(sourceFormat, n.getSourceFormat());
        assertEquals(d, n.getCaseDate());
        assertEquals(1, n.getSubstationCount());
        assertEquals(1, n.getVoltageLevelCount());
        Substation s = n.getSubstation("s" + num);
        assertNotNull(s);
        assertSame(merge, s.getNetwork());
        VoltageLevel vl = n.getVoltageLevel("vl" + num);
        assertNotNull(vl);
        assertSame(merge, vl.getNetwork());
        Bus b = n.getBusBreakerView().getBus("b" + num);
        assertNotNull(b);
        assertSame(merge, b.getNetwork());
        assertSame(n, merge.getSubstation("s" + num).getParentNetwork());
        assertSame(n, merge.getVoltageLevel("vl" + num).getParentNetwork());
        assertSame(n, merge.getBusBreakerView().getBus("b" + num).getParentNetwork());
        Load l = merge.getLoad("l" + num);
        Load other = merge.getLoad("l" + (3 - num));
        assertNotNull(l);
        assertNotNull(other);
        assertSame(n, l.getParentNetwork());
        Load lBis = n.getLoad("l" + num);
        assertSame(l, lBis);
        Load otherBis = n.getLoad("l" + (3 - num));
        assertNull(otherBis);
    }

    @Test
    public void checkMergingSameFormat() {
        Network merge = Network.merge(MERGE, n0, n1);
        assertEquals(MERGE, merge.getId());
        assertEquals("asdf", merge.getSourceFormat());
    }

    @Test
    public void checkMergingDifferentFormat() {
        Network merge = Network.merge(n1, n2);
        assertEquals(MERGE_DEFAULT_ID, merge.getId());
        assertEquals("hybrid", merge.getSourceFormat());
    }

    @Test
    public void mergeThenCloneVariantBug() {
        addCommonSubstationsAndVoltageLevels();
        addCommonDanglingLines("dl1", "code", "dl2", "code");
        Load ld2 = n2.getVoltageLevel("vl2").newLoad()
                .setId("ld2")
                .setConnectableBus("b2")
                .setBus("b2")
                .setP0(0.0)
                .setQ0(0.0)
                .add();
        Network merge = Network.merge(n1, n2);
        merge.getVariantManager().cloneVariant(VariantManagerConstants.INITIAL_VARIANT_ID, "test");
        merge.getVariantManager().setWorkingVariant("test");
        ld2.setP0(10);
        merge.getVariantManager().setWorkingVariant(VariantManagerConstants.INITIAL_VARIANT_ID);
        assertEquals(0, ld2.getP0(), 0);
    }

    @Test
    public void invertDanglingLinesWhenCreatingATieLine() {
        addCommonSubstationsAndVoltageLevels();
        addCommonDanglingLines("dl2", "code", "dl1", "code");
        Network merge = Network.merge(n1, n2);
        assertEquals(1, merge.getTieLineCount());
        TieLine tieLine = merge.getTieLine("dl1 + dl2");
        assertNotNull(tieLine);
        assertEquals("dl1", tieLine.getDanglingLine1().getId());
        assertEquals("dl2", tieLine.getDanglingLine2().getId());
    }

    @Test
    public void dontCreateATieLineWithAlreadyMergedDanglingLinesInMergedNetwork() {
        // The code is not the same if the dangling line with the duplicate pairing key is in the current network
        // or in the network we are adding when we try to associate dangling lines.
        //
        // This test covers the case where the duplicate pairing key is in the CURRENT network.
        addCommonSubstationsAndVoltageLevels();
        addCommonDanglingLines("dl1", "code", "dl2", "code");
        addDanglingLine(n1, "vl1", "dl3", "code", "b1", "b1");
        n1.newTieLine()
                .setId("dl1 + dl3")
                .setDanglingLine1("dl1")
                .setDanglingLine2("dl3")
                .add();
        Network merge = Network.merge(n1, n2);
        assertNotNull(merge.getTieLine("dl1 + dl3"));
        assertNull(merge.getTieLine("dl1 + dl2"));
        assertNull(merge.getTieLine("dl2 + dl3"));
        assertEquals(1, merge.getTieLineCount());
    }

    @Test
    public void dontCreateATieLineWithAlreadyMergedDanglingLinesInMergingNetwork() {
        // The code is not the same if the dangling line with the duplicate pairing key is in the current network
        // or in the network we are adding when we try to associate dangling lines.
        //
        // This test covers the case where the duplicate pairing key is in the ADDED network.
        addCommonSubstationsAndVoltageLevels();
        addCommonDanglingLines("dl1", "code", "dl2", "code");
        addDanglingLine(n2, "vl2", "dl3", "code", "b2", "b2");
        n2.newTieLine()
                .setId("dl2 + dl3")
                .setDanglingLine1("dl2")
                .setDanglingLine2("dl3")
                .add();
        Network merge = Network.merge(n1, n2);
        assertNotNull(merge.getTieLine("dl2 + dl3"));
        assertNull(merge.getTieLine("dl1 + dl2"));
        assertNull(merge.getTieLine("dl1 + dl3"));
        assertEquals(1, merge.getTieLineCount());
    }

    @Test
    public void multipleDanglingLinesInMergedNetwork() {
        // The code is not the same if the dangling line with the duplicate pairing key is in the current network
        // or in the network we are adding when we try to associate dangling lines.
        //
        // This test covers the case where the duplicate pairing key is in the CURRENT network.
        addCommonSubstationsAndVoltageLevels();
        addCommonDanglingLines("dl1", "code", "dl2", "code");
        addDanglingLine(n2, "vl2", "dl3", "code", "b2", null);
        Network merge = Network.merge(n1, n2);
        // Since n2 has ONLY ONE connected dangling line with the pairing key, the tie line is created
        assertNotNull(merge.getTieLine("dl1 + dl2"));
        assertEquals("dl1_name + dl2_name", merge.getTieLine("dl1 + dl2").getOptionalName().orElse(null));
        assertEquals("dl1_name + dl2_name", merge.getTieLine("dl1 + dl2").getNameOrId());
    }

    @Test
    public void multipleConnectedDanglingLinesInMergedNetwork() {
        // The code is not the same if the dangling line with the duplicate pairing key is in the current network
        // or in the network we are adding when we try to associate dangling lines.
        //
        // This test covers the case where the duplicate pairing key is in the CURRENT network.
        addCommonSubstationsAndVoltageLevels();
        addCommonDanglingLines("dl1", "code", "dl2", "code");
        addDanglingLine(n2, "vl2", "dl3", "code", "b2", "b2");
        Network merge = Network.merge(n1, n2);
        // Since n2 has SEVERAL connected dangling lines with the pairing key, we don't create the tie line
        assertEquals(0, merge.getTieLineCount());
    }

    @Test
    public void multipleDanglingLinesInMergingNetwork() {
        // The code is not the same if the dangling line with the duplicate pairing key is in the current network
        // or in the network we are adding when we try to associate dangling lines.
        //
        // This test covers the case where the duplicate pairing key is in the ADDED network.
        addCommonSubstationsAndVoltageLevels();
        addCommonDanglingLines("dl1", "code", "dl2", "code");
        addDanglingLine(n1, "vl1", "dl3", "code", "b1", null);
        Network merge = Network.merge(n1, n2);
        // Since n1 has ONLY ONE connected dangling line with the pairing key, the tie line is created
        assertNotNull(merge.getTieLine("dl1 + dl2"));
        assertEquals("dl1_name + dl2_name", merge.getTieLine("dl1 + dl2").getOptionalName().orElse(null));
        assertEquals("dl1_name + dl2_name", merge.getTieLine("dl1 + dl2").getNameOrId());
    }

    @Test
    public void multipleConnectedDanglingLinesWithSamePairingKey() {
        // The code is not the same if the dangling line with the duplicate pairing key is in the current network
        // or in the network we are adding when we try to associate dangling lines.
        //
        // This test covers the case where the duplicate pairing key is in the ADDED network.
        addCommonSubstationsAndVoltageLevels();
        addCommonDanglingLines("dl1", "code", "dl2", "code");
        addDanglingLine(n1, "vl1", "dl3", "code", "b1", "b1");
        Network merge = Network.merge(n1, n2);
        // Since n1 has SEVERAL connected dangling lines with the pairing key, we don't create the tie line
        assertEquals(0, merge.getTieLineCount());
    }
}