CommonGridModelExportTest.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/.
 * SPDX-License-Identifier: MPL-2.0
 */
package com.powsybl.cgmes.conversion.test.export;

import com.powsybl.cgmes.conformity.CgmesConformity1Catalog;
import com.powsybl.cgmes.conversion.CgmesExport;
import com.powsybl.cgmes.extensions.CgmesMetadataModels;
import com.powsybl.cgmes.extensions.CgmesMetadataModelsAdder;
import com.powsybl.cgmes.model.CgmesMetadataModel;
import com.powsybl.cgmes.model.CgmesNamespace;
import com.powsybl.cgmes.model.CgmesSubset;
import com.powsybl.commons.datasource.DataSource;
import com.powsybl.commons.datasource.DirectoryDataSource;
import com.powsybl.commons.datasource.MemDataSource;
import com.powsybl.commons.datasource.ReadOnlyDataSource;
import com.powsybl.commons.report.PowsyblCoreReportResourceBundle;
import com.powsybl.commons.test.PowsyblCoreTestReportResourceBundle;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.commons.test.AbstractSerDeTest;
import com.powsybl.iidm.network.*;
import org.junit.jupiter.api.Test;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.regex.Pattern;

import static com.powsybl.cgmes.conversion.test.ConversionUtil.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

/**
 * Summary from CGM Building Process Implementation Guide:
 * A CGM is created by assembling a set of IGMs for the same scenarioTime.
 * A Merging Agent is responsible for building the CGM.
 * The IGMs are first validated for plausibility by solving a power flow.
 * The CGM is then assembled from IGMs and a power flow is solved,
 * potentially adjusting some of the IGMs power flow hypothesis.
 * The Merging Agent provides an updated SSH for each IGM,
 * containing a md:Model.Supersedes reference to the IGM���s original SSH CIMXML file
 * and a single SV for the whole CGM, containing the results of power flow calculation.
 * The SV file must contain a md:Model.DependentOn reference to each IGM���s updated SSH.
 * The power flow of a CGM is calculated without topology processing. No new TP CIMXML
 * files are created. IGM TP files are used as an input. No TP fie is created as the result of CGM building.
 * This means that the CGM SV file must contain a md:Model.DependentOn reference to each IGM���s original TP.
 *
 * @author Luma Zamarre��o {@literal <zamarrenolm at aia.es>}
 */
class CommonGridModelExportTest extends AbstractSerDeTest {
    private static final Pattern REGEX_SCENARIO_TIME = Pattern.compile("Model.scenarioTime>(.*?)<");
    private static final Pattern REGEX_DESCRIPTION = Pattern.compile("Model.description>(.*?)<");
    private static final Pattern REGEX_VERSION = Pattern.compile("Model.version>(.*?)<");
    private static final Pattern REGEX_DEPENDENT_ON = Pattern.compile("Model.DependentOn rdf:resource=\"(.*?)\"");
    private static final Pattern REGEX_SUPERSEDES = Pattern.compile("Model.Supersedes rdf:resource=\"(.*?)\"");
    private static final Pattern REGEX_PROFILE = Pattern.compile("Model.profile>(.*?)<");
    private static final Pattern REGEX_MAS = Pattern.compile("Model.modelingAuthoritySet>(.*?)<");

    @Test
    void testIgmExportNoModelsNoPropertiesVersion() throws IOException {
        Network network = bareNetwork2Subnetworks();
        // Perform a simple IGM export and read the exported files
        Properties exportParams = new Properties();
        exportParams.put(CgmesExport.CGM_EXPORT, false);
        String basename = "test_bare_igm_be";
        network.getSubnetwork("Network_BE").write("CGMES", exportParams, tmpDir.resolve(basename));
        String exportedBeSshXml = Files.readString(tmpDir.resolve(basename + "_SSH.xml"));
        String exportedBeSvXml = Files.readString(tmpDir.resolve(basename + "_SV.xml"));

        // There is no version number for original models, so if exported as IGM they would have version equals to 1
        assertEquals("1", getFirstMatch(exportedBeSshXml, REGEX_VERSION));
        assertEquals("1", getFirstMatch(exportedBeSvXml, REGEX_VERSION));
    }

    @Test
    void testCgmExportWithModelsVersion() throws IOException {
        Network network = bareNetwork2Subnetworks();
        addModelsForSubnetworks(network, 0);

        // Perform a CGM export and read the exported files
        Properties exportParams = new Properties();
        exportParams.put(CgmesExport.CGM_EXPORT, true);
        String basename = "test_bare_models_0";
        network.write("CGMES", exportParams, tmpDir.resolve(basename));
        String updatedBeSshXml = Files.readString(tmpDir.resolve(basename + "_BE_SSH.xml"));
        String updatedNlSshXml = Files.readString(tmpDir.resolve(basename + "_NL_SSH.xml"));
        String updatedCgmSvXml = Files.readString(tmpDir.resolve(basename + "_SV.xml"));

        // Version number should be increased from original models and be the same for all instance files
        assertEquals("1", getFirstMatch(updatedBeSshXml, REGEX_VERSION));
        assertEquals("1", getFirstMatch(updatedNlSshXml, REGEX_VERSION));
        assertEquals("1", getFirstMatch(updatedCgmSvXml, REGEX_VERSION));
    }

    @Test
    void testCgmExportNoModelsNoProperties() throws IOException {
        // Create a simple network with two subnetworks
        Network network = bareNetwork2Subnetworks();

        // Perform a CGM export and read the exported files
        Properties exportParams = new Properties();
        exportParams.put(CgmesExport.CGM_EXPORT, true);
        String basename = "test_bare";
        network.write("CGMES", exportParams, tmpDir.resolve(basename));
        String updatedBeSshXml = Files.readString(tmpDir.resolve(basename + "_BE_SSH.xml"));
        String updatedNlSshXml = Files.readString(tmpDir.resolve(basename + "_NL_SSH.xml"));
        String updatedCgmSvXml = Files.readString(tmpDir.resolve(basename + "_SV.xml"));

        // Scenario time should be the same for all models
        assertEquals("2021-02-03T04:30:00Z", getFirstMatch(updatedBeSshXml, REGEX_SCENARIO_TIME));
        assertEquals("2021-02-03T04:30:00Z", getFirstMatch(updatedNlSshXml, REGEX_SCENARIO_TIME));
        assertEquals("2021-02-03T04:30:00Z", getFirstMatch(updatedCgmSvXml, REGEX_SCENARIO_TIME));

        // Description should be the default one
        assertEquals("SSH Model", getFirstMatch(updatedBeSshXml, REGEX_DESCRIPTION));
        assertEquals("SSH Model", getFirstMatch(updatedNlSshXml, REGEX_DESCRIPTION));
        assertEquals("SV Model", getFirstMatch(updatedCgmSvXml, REGEX_DESCRIPTION));

        // There is no version number for original models, so if exported as IGM they would have version equals to 1
        // Version number for updated models is increased by 1, so it equals to 2 in the end
        assertEquals("2", getFirstMatch(updatedBeSshXml, REGEX_VERSION));
        assertEquals("2", getFirstMatch(updatedNlSshXml, REGEX_VERSION));
        assertEquals("2", getFirstMatch(updatedCgmSvXml, REGEX_VERSION));

        // The updated CGM SV should depend on the updated IGMs SSH and on the original IGMs TP
        // Here the version number part of the id 1 for original models and 2 for updated ones
        String updatedBeSshId = "urn:uuid:Network_BE_N_STEADY_STATE_HYPOTHESIS_2021-02-03T04:30:00Z_2_1D__FM";
        String updatedNlSshId = "urn:uuid:Network_NL_N_STEADY_STATE_HYPOTHESIS_2021-02-03T04:30:00Z_2_1D__FM";
        String originalBeTpId = "urn:uuid:Network_BE_N_TOPOLOGY_2021-02-03T04:30:00Z_1_1D__FM";
        String originalNlTpId = "urn:uuid:Network_NL_N_TOPOLOGY_2021-02-03T04:30:00Z_1_1D__FM";
        String originalBeTpBdId = "urn:uuid:Network_BE_N_TOPOLOGY_BOUNDARY_2021-02-03T04:30:00Z_1_1D__FM";
        String originalNlTpBdId = "urn:uuid:Network_NL_N_TOPOLOGY_BOUNDARY_2021-02-03T04:30:00Z_1_1D__FM";
        Set<String> expectedDependencies = Set.of(updatedBeSshId, updatedNlSshId, originalBeTpId, originalNlTpId, originalBeTpBdId, originalNlTpBdId);
        assertEquals(expectedDependencies, getUniqueMatches(updatedCgmSvXml, REGEX_DEPENDENT_ON));

        // Each updated IGM SSH should supersede the original one and depend on the original EQ
        String originalBeSshId = "urn:uuid:Network_BE_N_STEADY_STATE_HYPOTHESIS_2021-02-03T04:30:00Z_1_1D__FM";
        String originalBeEqId = "urn:uuid:Network_BE_N_EQUIPMENT_2021-02-03T04:30:00Z_1_1D__FM";
        String originalNlSshId = "urn:uuid:Network_NL_N_STEADY_STATE_HYPOTHESIS_2021-02-03T04:30:00Z_1_1D__FM";
        String originalNlEqId = "urn:uuid:Network_NL_N_EQUIPMENT_2021-02-03T04:30:00Z_1_1D__FM";
        assertEquals(originalBeSshId, getFirstMatch(updatedBeSshXml, REGEX_SUPERSEDES));
        assertEquals(originalBeEqId, getFirstMatch(updatedBeSshXml, REGEX_DEPENDENT_ON));
        assertEquals(originalNlSshId, getFirstMatch(updatedNlSshXml, REGEX_SUPERSEDES));
        assertEquals(originalNlEqId, getFirstMatch(updatedNlSshXml, REGEX_DEPENDENT_ON));

        // Profiles should be consistent with the instance files
        assertEquals("http://entsoe.eu/CIM/SteadyStateHypothesis/1/1", getFirstMatch(updatedBeSshXml, REGEX_PROFILE));
        assertEquals("http://entsoe.eu/CIM/SteadyStateHypothesis/1/1", getFirstMatch(updatedNlSshXml, REGEX_PROFILE));
        assertEquals("http://entsoe.eu/CIM/StateVariables/4/1", getFirstMatch(updatedCgmSvXml, REGEX_PROFILE));

        // All MAS should be equal to the default one since none has been provided
        assertEquals("powsybl.org", getFirstMatch(updatedBeSshXml, REGEX_MAS));
        assertEquals("powsybl.org", getFirstMatch(updatedNlSshXml, REGEX_MAS));
        assertEquals("powsybl.org", getFirstMatch(updatedCgmSvXml, REGEX_MAS));
    }

    @Test
    void testCgmExportWithModelsForSubnetworks() throws IOException {
        // Create a simple network with two subnetworks
        Network network = bareNetwork2Subnetworks();
        addModelsForSubnetworks(network, 1);

        // Perform a CGM export and read the exported files
        Properties exportParams = new Properties();
        exportParams.put(CgmesExport.CGM_EXPORT, true);
        String basename = "test_bare+submodels";
        network.write("CGMES", exportParams, tmpDir.resolve(basename));
        String updatedBeSshXml = Files.readString(tmpDir.resolve(basename + "_BE_SSH.xml"));
        String updatedNlSshXml = Files.readString(tmpDir.resolve(basename + "_NL_SSH.xml"));
        String updatedCgmSvXml = Files.readString(tmpDir.resolve(basename + "_SV.xml"));

        // Scenario time should be the same for all models
        assertEquals("2021-02-03T04:30:00Z", getFirstMatch(updatedBeSshXml, REGEX_SCENARIO_TIME));
        assertEquals("2021-02-03T04:30:00Z", getFirstMatch(updatedNlSshXml, REGEX_SCENARIO_TIME));
        assertEquals("2021-02-03T04:30:00Z", getFirstMatch(updatedCgmSvXml, REGEX_SCENARIO_TIME));

        // IGM descriptions should be the ones provided in subnetwork models, CGM description should be the default one
        assertEquals("BE network description", getFirstMatch(updatedBeSshXml, REGEX_DESCRIPTION));
        assertEquals("NL network description", getFirstMatch(updatedNlSshXml, REGEX_DESCRIPTION));
        assertEquals("SV Model", getFirstMatch(updatedCgmSvXml, REGEX_DESCRIPTION));

        // Version number should be increased from original models and be the same for all instance files
        assertEquals("2", getFirstMatch(updatedBeSshXml, REGEX_VERSION));
        assertEquals("2", getFirstMatch(updatedNlSshXml, REGEX_VERSION));
        assertEquals("2", getFirstMatch(updatedCgmSvXml, REGEX_VERSION));

        // The updated CGM SV should depend on the updated IGMs SSH and on the original IGMs TP
        String updatedBeSshId = "urn:uuid:Network_BE_N_STEADY_STATE_HYPOTHESIS_2021-02-03T04:30:00Z_2_1D__FM";
        String updatedNlSshId = "urn:uuid:Network_NL_N_STEADY_STATE_HYPOTHESIS_2021-02-03T04:30:00Z_2_1D__FM";
        String originalBeTpId = "urn:uuid:Network_BE_N_TOPOLOGY_2021-02-03T04:30:00Z_1_1D__FM";
        String originalNlTpId = "urn:uuid:Network_NL_N_TOPOLOGY_2021-02-03T04:30:00Z_1_1D__FM";
        String originalTpBdId = "Common TP_BD model ID";
        Set<String> expectedDependencies = Set.of(updatedBeSshId, updatedNlSshId, originalBeTpId, originalNlTpId, originalTpBdId);
        assertEquals(expectedDependencies, getUniqueMatches(updatedCgmSvXml, REGEX_DEPENDENT_ON));

        // Each updated IGM SSH should supersede the original one and depend on the original EQ
        String originalBeSshId = "urn:uuid:Network_BE_N_STEADY_STATE_HYPOTHESIS_2021-02-03T04:30:00Z_1_1D__FM";
        String originalNlSshId = "urn:uuid:Network_NL_N_STEADY_STATE_HYPOTHESIS_2021-02-03T04:30:00Z_1_1D__FM";
        assertEquals(originalBeSshId, getFirstMatch(updatedBeSshXml, REGEX_SUPERSEDES));
        assertEquals(originalNlSshId, getFirstMatch(updatedNlSshXml, REGEX_SUPERSEDES));
        assertEquals(Set.of("BE EQ model ID"), getUniqueMatches(updatedBeSshXml, REGEX_DEPENDENT_ON));
        assertEquals(Set.of("NL EQ model ID"), getUniqueMatches(updatedNlSshXml, REGEX_DEPENDENT_ON));

        // Profiles should be consistent with the instance files
        assertEquals("http://entsoe.eu/CIM/SteadyStateHypothesis/1/1", getFirstMatch(updatedBeSshXml, REGEX_PROFILE));
        assertEquals("http://entsoe.eu/CIM/SteadyStateHypothesis/1/1", getFirstMatch(updatedNlSshXml, REGEX_PROFILE));
        assertEquals("http://entsoe.eu/CIM/StateVariables/4/1", getFirstMatch(updatedCgmSvXml, REGEX_PROFILE));

        // IGM MAS should be the ones provided in subnetwork models, CGM MAS should be the default one
        assertEquals("http://elia.be/CGMES/2.4.15", getFirstMatch(updatedBeSshXml, REGEX_MAS));
        assertEquals("http://tennet.nl/CGMES/2.4.15", getFirstMatch(updatedNlSshXml, REGEX_MAS));
        assertEquals("powsybl.org", getFirstMatch(updatedCgmSvXml, REGEX_MAS));
    }

    @Test
    void testCgmExportWithModelsForAllNetworks() throws IOException {
        // Create a simple network with two subnetworks
        Network network = bareNetwork2Subnetworks();
        addModelsForSubnetworks(network, 1);
        addModelForNetwork(network, 3);

        // Perform a CGM export and read the exported files
        Properties exportParams = new Properties();
        exportParams.put(CgmesExport.CGM_EXPORT, true);
        String basename = "test_bare+models";
        network.write("CGMES", exportParams, tmpDir.resolve(basename));
        String updatedBeSshXml = Files.readString(tmpDir.resolve(basename + "_BE_SSH.xml"));
        String updatedNlSshXml = Files.readString(tmpDir.resolve(basename + "_NL_SSH.xml"));
        String updatedCgmSvXml = Files.readString(tmpDir.resolve(basename + "_SV.xml"));

        // The main network has a different scenario time than the subnetworks
        // All updated models should get that scenario time
        assertEquals("2022-03-04T05:30:00Z", getFirstMatch(updatedBeSshXml, REGEX_SCENARIO_TIME));
        assertEquals("2022-03-04T05:30:00Z", getFirstMatch(updatedNlSshXml, REGEX_SCENARIO_TIME));
        assertEquals("2022-03-04T05:30:00Z", getFirstMatch(updatedCgmSvXml, REGEX_SCENARIO_TIME));

        // IGM descriptions should be the ones provided in subnetwork models, CGM description should be the one provided in main network model
        assertEquals("BE network description", getFirstMatch(updatedBeSshXml, REGEX_DESCRIPTION));
        assertEquals("NL network description", getFirstMatch(updatedNlSshXml, REGEX_DESCRIPTION));
        assertEquals("Merged network description", getFirstMatch(updatedCgmSvXml, REGEX_DESCRIPTION));

        // The main network has a different version number (3) than the subnetworks (1)
        // Updated models should use next version taking into account the max version number of inputs (next version is 4)
        assertEquals("4", getFirstMatch(updatedBeSshXml, REGEX_VERSION));
        assertEquals("4", getFirstMatch(updatedNlSshXml, REGEX_VERSION));
        assertEquals("4", getFirstMatch(updatedCgmSvXml, REGEX_VERSION));

        // The updated CGM SV should depend on the updated IGMs SSH and on the original IGMs TP
        // The model of the main network brings an additional dependency
        String updatedBeSshId = "urn:uuid:Network_BE_N_STEADY_STATE_HYPOTHESIS_2022-03-04T05:30:00Z_4_1D__FM";
        String updatedNlSshId = "urn:uuid:Network_NL_N_STEADY_STATE_HYPOTHESIS_2022-03-04T05:30:00Z_4_1D__FM";
        String originalBeTpId = "urn:uuid:Network_BE_N_TOPOLOGY_2022-03-04T05:30:00Z_1_1D__FM";
        String originalNlTpId = "urn:uuid:Network_NL_N_TOPOLOGY_2022-03-04T05:30:00Z_1_1D__FM";
        String originalTpBdId = "Common TP_BD model ID";
        String additionalDependency = "Additional dependency";
        Set<String> expectedDependencies = Set.of(updatedBeSshId, updatedNlSshId, originalBeTpId,
                originalNlTpId, originalTpBdId, additionalDependency);
        assertEquals(expectedDependencies, getUniqueMatches(updatedCgmSvXml, REGEX_DEPENDENT_ON));

        // Each updated IGM SSH should supersede the original one and depend on the original EQ
        String originalBeSshId = "urn:uuid:Network_BE_N_STEADY_STATE_HYPOTHESIS_2022-03-04T05:30:00Z_1_1D__FM";
        String originalNlSshId = "urn:uuid:Network_NL_N_STEADY_STATE_HYPOTHESIS_2022-03-04T05:30:00Z_1_1D__FM";
        assertEquals(originalBeSshId, getFirstMatch(updatedBeSshXml, REGEX_SUPERSEDES));
        assertEquals(originalNlSshId, getFirstMatch(updatedNlSshXml, REGEX_SUPERSEDES));
        assertEquals(Set.of("BE EQ model ID"), getUniqueMatches(updatedBeSshXml, REGEX_DEPENDENT_ON));
        assertEquals(Set.of("NL EQ model ID"), getUniqueMatches(updatedNlSshXml, REGEX_DEPENDENT_ON));

        // Profiles should be consistent with the instance files
        // The model of the main network brings an additional profile
        assertEquals("http://entsoe.eu/CIM/SteadyStateHypothesis/1/1", getFirstMatch(updatedBeSshXml, REGEX_PROFILE));
        assertEquals("http://entsoe.eu/CIM/SteadyStateHypothesis/1/1", getFirstMatch(updatedNlSshXml, REGEX_PROFILE));
        Set<String> expectedProfiles = Set.of("Additional profile", "http://entsoe.eu/CIM/StateVariables/4/1");
        assertEquals(expectedProfiles, getUniqueMatches(updatedCgmSvXml, REGEX_PROFILE));

        // IGM MAS should be the ones provided in subnetwork models, CGM MAS should be the one provided in main network model
        assertEquals("http://elia.be/CGMES/2.4.15", getFirstMatch(updatedBeSshXml, REGEX_MAS));
        assertEquals("http://tennet.nl/CGMES/2.4.15", getFirstMatch(updatedNlSshXml, REGEX_MAS));
        assertEquals("Modeling Authority", getFirstMatch(updatedCgmSvXml, REGEX_MAS));
    }

    @Test
    void testCgmExportWithProperties() throws IOException {
        // Create a simple network with two subnetworks
        Network network = bareNetwork2Subnetworks();

        // Perform a CGM export and read the exported files
        Properties exportParams = new Properties();
        exportParams.put(CgmesExport.CGM_EXPORT, true);
        exportParams.put(CgmesExport.MODELING_AUTHORITY_SET, "Regional Coordination Center");
        exportParams.put(CgmesExport.MODEL_DESCRIPTION, "Common Grid Model export");
        exportParams.put(CgmesExport.MODEL_VERSION, "4");
        exportParams.put(CgmesExport.BOUNDARY_TP_ID, "ENTSOE TP_BD model ID");
        String basename = "test_bare+properties";
        network.write("CGMES", exportParams, tmpDir.resolve(basename));
        String updatedBeSshXml = Files.readString(tmpDir.resolve(basename + "_BE_SSH.xml"));
        String updatedNlSshXml = Files.readString(tmpDir.resolve(basename + "_NL_SSH.xml"));
        String updatedCgmSvXml = Files.readString(tmpDir.resolve(basename + "_SV.xml"));

        // Scenario time should be the same for all models
        assertEquals("2021-02-03T04:30:00Z", getFirstMatch(updatedBeSshXml, REGEX_SCENARIO_TIME));
        assertEquals("2021-02-03T04:30:00Z", getFirstMatch(updatedNlSshXml, REGEX_SCENARIO_TIME));
        assertEquals("2021-02-03T04:30:00Z", getFirstMatch(updatedCgmSvXml, REGEX_SCENARIO_TIME));

        // Description should be the one provided as parameter and be the same for all instance files
        assertEquals("Common Grid Model export", getFirstMatch(updatedBeSshXml, REGEX_DESCRIPTION));
        assertEquals("Common Grid Model export", getFirstMatch(updatedNlSshXml, REGEX_DESCRIPTION));
        assertEquals("Common Grid Model export", getFirstMatch(updatedCgmSvXml, REGEX_DESCRIPTION));

        // Version number should be the one provided as parameter and be the same for all instance files
        assertEquals("4", getFirstMatch(updatedBeSshXml, REGEX_VERSION));
        assertEquals("4", getFirstMatch(updatedNlSshXml, REGEX_VERSION));
        assertEquals("4", getFirstMatch(updatedCgmSvXml, REGEX_VERSION));

        // The updated CGM SV should depend on the updated IGMs SSH and on the original IGMs TP
        String updatedBeSshId = "urn:uuid:Network_BE_N_STEADY_STATE_HYPOTHESIS_2021-02-03T04:30:00Z_4_1D__FM";
        String updatedNlSshId = "urn:uuid:Network_NL_N_STEADY_STATE_HYPOTHESIS_2021-02-03T04:30:00Z_4_1D__FM";
        String originalBeTpId = "urn:uuid:Network_BE_N_TOPOLOGY_2021-02-03T04:30:00Z_1_1D__FM";
        String originalNlTpId = "urn:uuid:Network_NL_N_TOPOLOGY_2021-02-03T04:30:00Z_1_1D__FM";
        String originalTpBdId = "ENTSOE TP_BD model ID";
        Set<String> expectedDependencies = Set.of(updatedBeSshId, updatedNlSshId, originalBeTpId, originalNlTpId, originalTpBdId);
        assertEquals(expectedDependencies, getUniqueMatches(updatedCgmSvXml, REGEX_DEPENDENT_ON));

        // Each updated IGM SSH should supersede the original one and depend on the original EQ
        String originalBeSshId = "urn:uuid:Network_BE_N_STEADY_STATE_HYPOTHESIS_2021-02-03T04:30:00Z_1_1D__FM";
        String originalBeEqId = "urn:uuid:Network_BE_N_EQUIPMENT_2021-02-03T04:30:00Z_1_1D__FM";
        String originalNlSshId = "urn:uuid:Network_NL_N_STEADY_STATE_HYPOTHESIS_2021-02-03T04:30:00Z_1_1D__FM";
        String originalNlEqId = "urn:uuid:Network_NL_N_EQUIPMENT_2021-02-03T04:30:00Z_1_1D__FM";
        assertEquals(originalBeSshId, getFirstMatch(updatedBeSshXml, REGEX_SUPERSEDES));
        assertEquals(originalBeEqId, getFirstMatch(updatedBeSshXml, REGEX_DEPENDENT_ON));
        assertEquals(originalNlSshId, getFirstMatch(updatedNlSshXml, REGEX_SUPERSEDES));
        assertEquals(originalNlEqId, getFirstMatch(updatedNlSshXml, REGEX_DEPENDENT_ON));

        // Profiles should be consistent with the instance files
        assertEquals("http://entsoe.eu/CIM/SteadyStateHypothesis/1/1", getFirstMatch(updatedBeSshXml, REGEX_PROFILE));
        assertEquals("http://entsoe.eu/CIM/SteadyStateHypothesis/1/1", getFirstMatch(updatedNlSshXml, REGEX_PROFILE));
        assertEquals("http://entsoe.eu/CIM/StateVariables/4/1", getFirstMatch(updatedCgmSvXml, REGEX_PROFILE));

        // IGM MAS should be the default ones, CGM MAS should be the one provided as parameter
        assertEquals("powsybl.org", getFirstMatch(updatedBeSshXml, REGEX_MAS));
        assertEquals("powsybl.org", getFirstMatch(updatedNlSshXml, REGEX_MAS));
        assertEquals("Regional Coordination Center", getFirstMatch(updatedCgmSvXml, REGEX_MAS));
    }

    @Test
    void testCgmExportWithModelsAndProperties() throws IOException {
        // Create a simple network with two subnetworks
        Network network = bareNetwork2Subnetworks();
        addModelsForSubnetworks(network, 1);
        addModelForNetwork(network, 2);

        // Perform a CGM export and read the exported files
        Properties exportParams = new Properties();
        exportParams.put(CgmesExport.CGM_EXPORT, true);
        exportParams.put(CgmesExport.MODELING_AUTHORITY_SET, "Regional Coordination Center");
        exportParams.put(CgmesExport.MODEL_DESCRIPTION, "Common Grid Model export");
        exportParams.put(CgmesExport.MODEL_VERSION, "4");
        exportParams.put(CgmesExport.BOUNDARY_TP_ID, "ENTSOE TP_BD model ID");
        String basename = "test_bare+models+properties";
        network.write("CGMES", exportParams, tmpDir.resolve(basename));
        String updatedBeSshXml = Files.readString(tmpDir.resolve(basename + "_BE_SSH.xml"));
        String updatedNlSshXml = Files.readString(tmpDir.resolve(basename + "_NL_SSH.xml"));
        String updatedCgmSvXml = Files.readString(tmpDir.resolve(basename + "_SV.xml"));

        // The main network has a different scenario time than the subnetworks
        // All updated models should get that scenario time
        assertEquals("2022-03-04T05:30:00Z", getFirstMatch(updatedBeSshXml, REGEX_SCENARIO_TIME));
        assertEquals("2022-03-04T05:30:00Z", getFirstMatch(updatedNlSshXml, REGEX_SCENARIO_TIME));
        assertEquals("2022-03-04T05:30:00Z", getFirstMatch(updatedCgmSvXml, REGEX_SCENARIO_TIME));

        // Both the models and a property define the description. The property should prevail.
        assertEquals("Common Grid Model export", getFirstMatch(updatedBeSshXml, REGEX_DESCRIPTION));
        assertEquals("Common Grid Model export", getFirstMatch(updatedNlSshXml, REGEX_DESCRIPTION));
        assertEquals("Common Grid Model export", getFirstMatch(updatedCgmSvXml, REGEX_DESCRIPTION));

        // Both the models and a property define the version number. The property should prevail.
        assertEquals("4", getFirstMatch(updatedBeSshXml, REGEX_VERSION));
        assertEquals("4", getFirstMatch(updatedNlSshXml, REGEX_VERSION));
        assertEquals("4", getFirstMatch(updatedCgmSvXml, REGEX_VERSION));

        // The updated CGM SV should depend on the updated IGMs SSH and on the original IGMs TP
        // The model of the main network brings an additional dependency
        String updatedBeSshId = "urn:uuid:Network_BE_N_STEADY_STATE_HYPOTHESIS_2022-03-04T05:30:00Z_4_1D__FM";
        String updatedNlSshId = "urn:uuid:Network_NL_N_STEADY_STATE_HYPOTHESIS_2022-03-04T05:30:00Z_4_1D__FM";
        String originalBeTpId = "urn:uuid:Network_BE_N_TOPOLOGY_2022-03-04T05:30:00Z_1_1D__FM";
        String originalNlTpId = "urn:uuid:Network_NL_N_TOPOLOGY_2022-03-04T05:30:00Z_1_1D__FM";
        String originalTpBdId = "ENTSOE TP_BD model ID";  // the parameter prevails on the extension
        String additionalDependency = "Additional dependency";
        Set<String> expectedDependencies = Set.of(updatedBeSshId, updatedNlSshId, originalBeTpId,
                originalNlTpId, originalTpBdId, additionalDependency);
        assertEquals(expectedDependencies, getUniqueMatches(updatedCgmSvXml, REGEX_DEPENDENT_ON));

        // Each updated IGM SSH should supersede the original one and depend on the original EQ
        String originalBeSshId = "urn:uuid:Network_BE_N_STEADY_STATE_HYPOTHESIS_2022-03-04T05:30:00Z_1_1D__FM";
        String originalNlSshId = "urn:uuid:Network_NL_N_STEADY_STATE_HYPOTHESIS_2022-03-04T05:30:00Z_1_1D__FM";
        assertEquals(originalBeSshId, getFirstMatch(updatedBeSshXml, REGEX_SUPERSEDES));
        assertEquals(originalNlSshId, getFirstMatch(updatedNlSshXml, REGEX_SUPERSEDES));
        assertEquals(Set.of("BE EQ model ID"), getUniqueMatches(updatedBeSshXml, REGEX_DEPENDENT_ON));
        assertEquals(Set.of("NL EQ model ID"), getUniqueMatches(updatedNlSshXml, REGEX_DEPENDENT_ON));

        // Profiles should be consistent with the instance files, CGM SV has an additional profile
        assertEquals("http://entsoe.eu/CIM/SteadyStateHypothesis/1/1", getFirstMatch(updatedBeSshXml, REGEX_PROFILE));
        assertEquals("http://entsoe.eu/CIM/SteadyStateHypothesis/1/1", getFirstMatch(updatedNlSshXml, REGEX_PROFILE));
        Set<String> expectedProfiles = Set.of("Additional profile", "http://entsoe.eu/CIM/StateVariables/4/1");
        assertEquals(expectedProfiles, getUniqueMatches(updatedCgmSvXml, REGEX_PROFILE));

        // Both the model and a property define the main network MAS. The property should prevail.
        assertEquals("http://elia.be/CGMES/2.4.15", getFirstMatch(updatedBeSshXml, REGEX_MAS));
        assertEquals("http://tennet.nl/CGMES/2.4.15", getFirstMatch(updatedNlSshXml, REGEX_MAS));
        assertEquals("Regional Coordination Center", getFirstMatch(updatedCgmSvXml, REGEX_MAS));
    }

    @Test
    void testFaraoUseCase() {
        // We use MicroGrid base case assembled as the CGM model to export
        ReadOnlyDataSource ds = CgmesConformity1Catalog.microGridBaseCaseAssembled().dataSource();
        Network cgmNetwork = Network.read(ds);

        // Check the version of the individual files in the test case
        int currentVersion = readVersion(ds, "MicroGridTestConfiguration_BC_BE_SSH_V2.xml").orElseThrow();
        int sshNLVersion = readVersion(ds, "MicroGridTestConfiguration_BC_NL_SSH_V2.xml").orElseThrow();
        int svVersion = readVersion(ds, "MicroGridTestConfiguration_BC_Assembled_SV_V2.xml").orElseThrow();
        assertEquals(currentVersion, sshNLVersion);
        assertEquals(currentVersion, svVersion);
        Network[] ns = cgmNetwork.getSubnetworks().toArray(new Network[] {});
        assertEquals(currentVersion, ns[0].getExtension(CgmesMetadataModels.class).getModelForSubset(CgmesSubset.STEADY_STATE_HYPOTHESIS).orElseThrow().getVersion());
        assertEquals(currentVersion, ns[1].getExtension(CgmesMetadataModels.class).getModelForSubset(CgmesSubset.STEADY_STATE_HYPOTHESIS).orElseThrow().getVersion());

        // If we export this CGM we expected the output files to on current version + 1
        int expectedOutputVersion = currentVersion + 1;

        // Export with the same parameters of the FARAO use case
        Properties exportParams = new Properties();
        exportParams.put(CgmesExport.EXPORT_BOUNDARY_POWER_FLOWS, true);
        exportParams.put(CgmesExport.NAMING_STRATEGY, "cgmes");
        exportParams.put(CgmesExport.CGM_EXPORT, true);
        exportParams.put(CgmesExport.UPDATE_DEPENDENCIES, true);
        exportParams.put(CgmesExport.MODELING_AUTHORITY_SET, "MAS1");

        // Export using a reporter to gather the exported model identifiers
        ReportNode report = ReportNode.newRootReportNode()
                .withResourceBundles(PowsyblCoreTestReportResourceBundle.TEST_BASE_NAME, PowsyblCoreReportResourceBundle.BASE_NAME)
                .withMessageTemplate("rootKey")
                .build();

        MemDataSource memDataSource = new MemDataSource();
        cgmNetwork.write(new ExportersServiceLoader(), "CGMES", exportParams, memDataSource, report);

        // Check the output
        Set<String> exportedModelIdsFromFiles = new HashSet<>();
        Map<String, String> exportedModelId2Subset = new HashMap<>();
        Map<String, String> exportedModelId2NetworkId = new HashMap<>();
        String cgmesId;
        for (Map.Entry<Country, String> entry : TSO_BY_COUNTRY.entrySet()) {
            Country country = entry.getKey();
            // For this unit test we do not need the TSO from the entry value
            String filenameFromCgmesExport = cgmNetwork.getNameOrId() + "_" + country.toString() + "_SSH.xml";

            // The CGM network does not have metadata models, only the subnetworks
            assertEquals((CgmesMetadataModels) null, cgmNetwork.getExtension(CgmesMetadataModels.class));

            // The metadata models in memory are NEVER updated after export (this is by design)
            // Look for the subnetwork of the current country
            Network nc = cgmNetwork.getSubnetworks().stream().filter(n -> country == n.getSubstations().iterator().next().getCountry().orElseThrow()).toList().get(0);
            int sshVersionInNetworkMetadataModels = nc.getExtension(CgmesMetadataModels.class).getModelForSubset(CgmesSubset.STEADY_STATE_HYPOTHESIS).orElseThrow().getVersion();
            assertEquals(currentVersion, sshVersionInNetworkMetadataModels);

            // To know the exported version we should read what has been written in the output files
            int sshVersionInOutput = readVersion(memDataSource, filenameFromCgmesExport).orElseThrow();
            assertEquals(expectedOutputVersion, sshVersionInOutput);
            cgmesId = readModelId(memDataSource, filenameFromCgmesExport);
            exportedModelIdsFromFiles.add(cgmesId);
            exportedModelId2Subset.put(cgmesId, CgmesSubset.STEADY_STATE_HYPOTHESIS.getIdentifier());
            exportedModelId2NetworkId.put(cgmesId, nc.getId());
        }
        String filenameFromCgmesExport = cgmNetwork.getNameOrId() + "_SV.xml";
        // We have to read it from inside the SV file ...
        int svVersionInOutput = readVersion(memDataSource, filenameFromCgmesExport).orElseThrow();
        assertEquals(expectedOutputVersion, svVersionInOutput);
        cgmesId = readModelId(memDataSource, filenameFromCgmesExport);
        exportedModelIdsFromFiles.add(cgmesId);
        exportedModelId2Subset.put(cgmesId, CgmesSubset.STATE_VARIABLES.getIdentifier());
        exportedModelId2NetworkId.put(cgmesId, cgmNetwork.getId());

        // Obtain exported model identifiers from reporter
        Set<String> exportedModelIdsFromReporter = new HashSet<>();
        for (ReportNode n : report.getChildren()) {
            if ("core.cgmes.conversion.ExportedCgmesId".equals(n.getMessageKey())) {
                cgmesId = n.getValue("cgmesId").orElseThrow().toString();
                exportedModelIdsFromReporter.add(cgmesId);
                String subset = n.getValue("cgmesSubset").orElseThrow().toString();
                String networkId = n.getValue("networkId").orElseThrow().toString();
                assertEquals(exportedModelId2Subset.get(cgmesId), subset);
                assertEquals(exportedModelId2NetworkId.get(cgmesId), networkId);
            }
        }
        assertFalse(exportedModelIdsFromReporter.isEmpty());
        assertEquals(exportedModelIdsFromReporter, exportedModelIdsFromFiles);
    }

    @Test
    void testFaraoUseCaseManualExport() throws IOException {
        Network cgmNetwork = bareNetwork2Subnetworks();
        addModelsForSubnetworks(cgmNetwork, 2);

        // We set the version manually
        int exportedVersion = 18;

        // Common export parameters
        Properties exportParams = new Properties();
        exportParams.put(CgmesExport.EXPORT_BOUNDARY_POWER_FLOWS, true);
        exportParams.put(CgmesExport.NAMING_STRATEGY, "cgmes");
        // We do not want a quick CGM export
        exportParams.put(CgmesExport.CGM_EXPORT, false);
        exportParams.put(CgmesExport.UPDATE_DEPENDENCIES, false);

        // For each subnetwork, prepare the metadata for SSH and export it
        Path tmpFolder = tmpDir.resolve("tmp-manual");
        String basename = "manualBase";
        Files.createDirectories(tmpFolder);
        for (Network n : cgmNetwork.getSubnetworks()) {
            String country = n.getSubstations().iterator().next().getCountry().orElseThrow().toString();
            CgmesMetadataModel sshModel = n.getExtension(CgmesMetadataModels.class).getModelForSubset(CgmesSubset.STEADY_STATE_HYPOTHESIS).orElseThrow();
            sshModel.clearDependencies()
                    .clearSupersedes()
                    .addDependentOn("myDependency")
                    .addSupersedes("mySupersede")
                    .setVersion(exportedVersion)
                    .setModelingAuthoritySet("myModellingAuthority");
            exportParams.put(CgmesExport.PROFILES, List.of("SSH"));
            n.write("CGMES", exportParams, new DirectoryDataSource(tmpFolder, basename + "_" + country));
        }

        // In the main network, CREATE the metadata for SV and export it
        cgmNetwork.newExtension(CgmesMetadataModelsAdder.class)
                .newModel()
                    .setSubset(CgmesSubset.STATE_VARIABLES)
                    .addProfile("http://entsoe.eu/CIM/StateVariables/4/1")
                    .setId("mySvId")
                    .setVersion(exportedVersion)
                    .setModelingAuthoritySet("myModellinAuthority")
                    .addDependentOn("mySvDependency1")
                    .addDependentOn("mySvDependency2")
                .add()
                .add();
        exportParams.put(CgmesExport.PROFILES, List.of("SV"));
        DataSource dataSource = new DirectoryDataSource(tmpFolder, basename);
        cgmNetwork.write("CGMES", exportParams, dataSource);

        int expectedOutputVersion = exportedVersion;
        for (Map.Entry<Country, String> entry : TSO_BY_COUNTRY.entrySet()) {
            Country country = entry.getKey();
            // For this unit test we do not need the TSO from the entry value
            String filenameFromCgmesExport = basename + "_" + country.toString() + "_SSH.xml";

            // To know the exported version we should read what has been written in the output files
            int sshVersionInOutput = readVersion(dataSource, filenameFromCgmesExport).orElseThrow();
            assertEquals(expectedOutputVersion, sshVersionInOutput);

            String outputSshXml = Files.readString(tmpFolder.resolve(filenameFromCgmesExport));
            assertEquals(Set.of("myDependency"), getUniqueMatches(outputSshXml, REGEX_DEPENDENT_ON));
            assertEquals(Set.of("mySupersede"), getUniqueMatches(outputSshXml, REGEX_SUPERSEDES));
        }
        String filenameFromCgmesExport = basename + "_SV.xml";
        // We read it from inside the SV file ...
        int svVersionInOutput = readVersion(dataSource, filenameFromCgmesExport).orElseThrow();
        assertEquals(expectedOutputVersion, svVersionInOutput);
        // Check the dependencies
        String outputSvXml = Files.readString(tmpFolder.resolve(filenameFromCgmesExport));
        assertEquals(Set.of("mySvDependency1", "mySvDependency2"), getUniqueMatches(outputSvXml, REGEX_DEPENDENT_ON));
    }

    private static final Map<Country, String> TSO_BY_COUNTRY = Map.of(
            Country.BE, "Elia",
            Country.NL, "Tennet");

    private static Optional<Integer> readVersion(ReadOnlyDataSource ds, String filename) {
        try {
            return readVersion(ds.newInputStream(filename));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static Optional<Integer> readVersion(InputStream is) {
        try {
            XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(is);
            while (reader.hasNext()) {
                int next = reader.next();
                if (next == XMLStreamConstants.START_ELEMENT && reader.getLocalName().equals("Model.version")) {
                    String version = reader.getElementText();
                    reader.close();
                    return Optional.of(Integer.parseInt(version));
                }
            }
            reader.close();
        } catch (XMLStreamException e) {
            throw new RuntimeException(e);
        }
        return Optional.empty();
    }

    private static String readModelId(ReadOnlyDataSource ds, String filename) {
        try {
            return readId(ds.newInputStream(filename));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static String readId(InputStream is) {
        try {
            XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(is);
            while (reader.hasNext()) {
                int next = reader.next();
                if (next == XMLStreamConstants.START_ELEMENT && reader.getLocalName().equals("FullModel")) {
                    String id = reader.getAttributeValue(CgmesNamespace.RDF_NAMESPACE, "about");
                    reader.close();
                    return id;
                }
            }
            reader.close();
        } catch (XMLStreamException e) {
            throw new RuntimeException(e);
        }
        return null;
    }

    private Network bareNetwork2Subnetworks() {
        Network network1 = Network
                .create("Network_BE", "test")
                .newSubstation().setId("Substation1").setCountry(Country.BE).add()
                .getNetwork();
        network1.setCaseDate(ZonedDateTime.parse("2021-02-03T04:30:00.000+00:00"));

        Network network2 = Network
                .create("Network_NL", "test")
                .newSubstation().setId("Substation2").setCountry(Country.NL).add()
                .getNetwork();
        network2.setCaseDate(ZonedDateTime.parse("2021-02-03T04:30:00.000+00:00"));

        return Network.merge(network1, network2);
    }

    private void addModelsForSubnetworks(Network network, int version) {
        // Add a model to the 2 Subnetworks
        network.getSubnetwork("Network_BE")
                .newExtension(CgmesMetadataModelsAdder.class)
                .newModel()
                .setSubset(CgmesSubset.STEADY_STATE_HYPOTHESIS)
                .setDescription("BE network description")
                .setVersion(version)
                .setModelingAuthoritySet("http://elia.be/CGMES/2.4.15")
                .addDependentOn("BE EQ model ID")
                .addSupersedes("BE SSH previous ID")
                .addProfile("http://entsoe.eu/CIM/SteadyStateHypothesis/1/1")
                .add()
                .newModel()
                .setId("BE EQ model ID")
                .setSubset(CgmesSubset.EQUIPMENT)
                .setVersion(1)
                .setModelingAuthoritySet("http://elia.be/CGMES/2.4.15")
                .addProfile("http://entsoe.eu/CIM/EquipmentCore/3/1")
                .add()
                .newModel()
                .setId("Common TP_BD model ID")
                .setSubset(CgmesSubset.TOPOLOGY_BOUNDARY)
                .setVersion(1)
                .setModelingAuthoritySet("http://www.entsoe.eu/OperationalPlanning")
                .addProfile("http://entsoe.eu/CIM/TopologyBoundary/3/1")
                .add()
                .add();
        network.getSubnetwork("Network_NL")
                .newExtension(CgmesMetadataModelsAdder.class)
                .newModel()
                .setSubset(CgmesSubset.STEADY_STATE_HYPOTHESIS)
                .setDescription("NL network description")
                .setVersion(version)
                .setModelingAuthoritySet("http://tennet.nl/CGMES/2.4.15")
                .addDependentOn("NL EQ model ID")
                .addSupersedes("NL SSH previous ID")
                .addProfile("http://entsoe.eu/CIM/SteadyStateHypothesis/1/1")
                .add()
                .newModel()
                .setId("NL EQ model ID")
                .setSubset(CgmesSubset.EQUIPMENT)
                .setVersion(1)
                .setModelingAuthoritySet("http://tennet.nl/CGMES/2.4.15")
                .addProfile("http://entsoe.eu/CIM/EquipmentCore/3/1")
                .add()
                .newModel()
                .setId("Common TP_BD model ID")
                .setSubset(CgmesSubset.TOPOLOGY_BOUNDARY)
                .setVersion(1)
                .setModelingAuthoritySet("http://www.entsoe.eu/OperationalPlanning")
                .addProfile("http://entsoe.eu/CIM/TopologyBoundary/3/1")
                .add()
                .add();
    }

    private void addModelForNetwork(Network network, int version) {
        // Add a model to the merged network
        network.setCaseDate(ZonedDateTime.parse("2022-03-04T05:30:00.000+00:00"))
                .newExtension(CgmesMetadataModelsAdder.class)
                .newModel()
                .setSubset(CgmesSubset.STATE_VARIABLES)
                .setDescription("Merged network description")
                .setVersion(version)
                .setModelingAuthoritySet("Modeling Authority")
                .addDependentOn("Additional dependency")
                .addProfile("Additional profile")
                .add()
                .add();
    }

}