/*
 * RiskScape™ Copyright New Zealand Institute for Earth Science Limited
 * (Earth Sciences New Zealand) is distributed for research purposes only
 * under the terms of AGPLv3.
 *
 * RiskScape™ Copyright 2026 New Zealand Institute for Earth Science
 * Limited (Earth Sciences New Zealand). All rights reserved. Source code
 * available under the AGPLv3.
 * 
 * This program is free software: you can redistribute it and/or modify it under
 *  the terms of the GNU Affero General Public License as published by the Free
 *  Software Foundation, either version 3 of the License, or (at your option) any
 *  later version.
 * 
 * This program is distributed for RESEARCH PURPOSES ONLY, in the hope that it will
 * be useful for research and education initiatives.
 * 
 * If you are not a researcher, or you are a researcher who wishes to use this
 * program on terms other than AGPLv3 (including those who wish to restrict the
 * distribution of any source code created using this program), please contact:
 * https://riskscape.org.nz
 * 
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.  You should have received a copy
 * of the GNU Affero General Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * By way of summary only, under the AGPLv3:
 *     • Permissions of this strongest copyleft license are conditioned
 *       on making available complete source code of licensed works and
 *       modifications, which include larger works using a licensed work,
 *       under the same license.
 *     • Copyright and license notices must be preserved.
 *     • Contributors provide an express grant of patent rights.
 *     • When a modified version is used to provide a service over a
 *       network, the complete source code of the modified version must be made
 *       available.
 */
package nz.org.riskscape.engine.cli.tests;

import static nz.org.riskscape.engine.Assert.*;
import static nz.org.riskscape.engine.Matchers.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.util.Arrays;
import java.util.List;

import org.hamcrest.Matcher;
import org.hamcrest.core.IsAnything;
import org.junit.Ignore;
import org.junit.Test;

import com.google.common.collect.Lists;

import nz.org.riskscape.engine.Assert;
import nz.org.riskscape.engine.GeoHelper;
import nz.org.riskscape.engine.Matchers;
import nz.org.riskscape.engine.bind.ParamProblems;
import nz.org.riskscape.engine.bind.Parameter;
import nz.org.riskscape.engine.cli.ExitException;
import nz.org.riskscape.engine.data.ResolvedBookmark;
import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.problem.Problem.Severity;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.rl.ast.ExpressionProblems;
import nz.org.riskscape.wizard.WizardProblems;
import nz.org.riskscape.wizard.model2.smp.SampleType;

/**
 * Tests various paths through the wizard by loading/running models from saved config
 */
public class ModelRunWizardCommandTest extends BaseModelRunCommandTest {

  @Test
  public void canRunPointBasedExposureModel() throws Exception {
    runCommand.modelId = "point-assets";
    runCommand.run();

    // check that the pipeline file has some content. we're just checking it isn't empty here
    assertThat(openFile("pipeline.txt"), containsString("input"));

    // check model outputs the expected exposed buildings (in order because they're sorted)
    List<List<String>> rows = openCsv("event-impact.csv", "building_name");
    assertThat(rows,
        contains(
            contains(equalTo("Natural History Museum")),
            contains(equalTo("New York Public Library")),
            contains(equalTo("Statue of Liberty")),
            contains(equalTo("Trump Tower"))));
  }

  @Test
  public void warningForUnknownParameters() throws Exception {
    runCommand.modelId = "point-assets";
    runCommand.parameters = Lists.newArrayList("unknown=20");
    runCommand.run();

    assertThat(render(terminal.getCollectedProblems()),
        containsString(render(ParamProblems.get().ignored("[unknown]"))));
  }

  @Test
  public void canRunPointBasedExposureModelWithArea() throws Exception {
    // the path we take here through the wizard is of note: we're using the centroid
    // of polygons, so are skipping the sampling question-set, but we have an area-layer,
    // so still want to do the area sampling in the pipeline
    runCommand.modelId = "point-assets";
    runCommand.parameters = Arrays.asList(
        // so if you really really need to add an area layer into an existing wizard model you can
        // but it requires a bit more effort than most people could muster.
        "wizard.question-choice-3=input-areas",
        "wizard.question-choice-4=skip",
        "wizard.question-choice-5=sample",
        "wizard.question-choice-6=analysis",
        "wizard.question-choice-7=report-event-impact",
        "input-areas.layer=nyc_boroughs",
        "input-areas.geoprocess=false",
        "report-event-impact.select[0]=exposure.name as building_name",
        "report-event-impact.select[1]=area.name as area_name");
    runCommand.run();

    // check model outputs the expected exposed buildings *and* area
    List<List<String>> rows = openCsv("event-impact.csv", "building_name", "area_name");
    assertThat(rows,
        containsInAnyOrder(
            contains(equalTo("Statue of Liberty"), equalTo("Liberty Island")),
            contains(equalTo("Natural History Museum"), equalTo("Manhattan")),
            contains(equalTo("New York Public Library"), equalTo("Manhattan")),
            contains(equalTo("Trump Tower"), equalTo("Manhattan"))));
  }

  @Test
  public void canRunCentroidExposureModel() throws Exception {
    // we get the same results as canRunPointBasedExposureModel() except we choose to use
    // centroid sampling (rather than having no alternative) and use a different function
    runCommand.modelId = "centroid";
    runCommand.run();

    // check model outputs the expected exposed buildings
    List<List<String>> rows = openCsv("event-impact.csv", "name");
    assertThat(rows,
        containsInAnyOrder(
            contains(equalTo("Statue of Liberty")),
            contains(equalTo("Natural History Museum")),
            contains(equalTo("New York Public Library")),
            contains(equalTo("Trump Tower"))));
  }

  @Test
  public void canRunBuildingsFileExposureModelWithParametersFile() throws Exception {
    // we get the same results as canRunCentroidExposureModel.
    runCommand.modelId = "buildings-fire";
    runCommand.parametersFile = stdhome().resolve("parameters-building-fire-centroid.ini");
    runCommand.run();

    // check model outputs the expected exposed buildings
    List<List<String>> rows = openCsv("event-impact.csv", "name");
    assertThat(rows,
        containsInAnyOrder(
            contains(equalTo("Statue of Liberty")),
            contains(equalTo("Natural History Museum")),
            contains(equalTo("New York Public Library")),
            contains(equalTo("Trump Tower"))));
  }

  @Test
  public void canRunBuildingsFileExposureModelWithParametersFileAndOverrideParameter() throws Exception {
    // we get the same results as canRunBuildingsFileExposureModelWithParametersFile except the --parameter
    // changes the output column
    runCommand.modelId = "buildings-fire";
    runCommand.parametersFile = stdhome().resolve("parameters-building-fire-centroid.ini");
    runCommand.parameters = Lists.newArrayList("report-event-impact.select[0]=exposure.name as name");
    runCommand.run();

    // check model outputs the expected exposed buildings
    List<List<String>> rows = openCsv("event-impact.csv", "name");
    assertThat(rows,
        containsInAnyOrder(
            contains(equalTo("Statue of Liberty")),
            contains(equalTo("Natural History Museum")),
            contains(equalTo("New York Public Library")),
            contains(equalTo("Trump Tower"))));
  }

  @Test
  public void canAdaptComplexHazardToFunctionThatExpectsSimpleHazard() throws Exception {
    runCommand.modelId = "centroid";
    runCommand.parameters = Arrays.asList(
        "analysis.map-hazard=true",
        "analysis.map-hazard-intensity=severity", // use the severity attr from kaiju_fire
        "analysis.function=dummy",    // the dummy() function expects a simple hazard
        "report-event-impact.filter=true",        // dummy() does not return the same consequence that is being filtered
        "report-event-impact.select[1]=consequence");
    runCommand.run();

    List<List<String>> rows = openCsv("event-impact.csv", "name", "consequence");
    assertThat(rows.size(), is(26));
    assertThat(rows, hasItem(Arrays.asList("Trump Tower","9.0")));
  }

  @Test
  public void canRunSpecifyShapefileSaveFormatForModel() throws Exception {
    runCommand.modelId = "centroid";
    runCommand.parameters = Arrays.asList(
        // we need to include geometry in order to save it as a shapefile
        "report-event-impact.select[0]=exposure.geom",
        "report-event-impact.format=shapefile");
    runCommand.run();
    assertTrue(getTempDirectory().resolve("event-impact.shp").toFile().exists());
  }

  @Test
  public void canRunSpecifyCsvSaveFormatForModel() throws Exception {
    runCommand.modelId = "centroid";
    runCommand.parameters = Arrays.asList(
        // it should actually default to shapefile if geometry is present, so check we
        // can override this
        "report-event-impact.select[0]=exposure.geom",
        "report-event-impact.format=csv");
    runCommand.run();
    List<List<String>> rows = openCsv("event-impact.csv", "geom");
    assertThat(rows.size(), is(4));
  }

  @Test
  public void canAggregateResultsNamingAggregatesAndGroups() throws Exception {
    runCommand.modelId = "aggregate-results";
    runCommand.run();

    // check model outputs the expected exposed buildings
    List<List<String>> rows = openCsv("event-impact.csv", "area", "construction", "severity", "resilience", "damage");
    assertThat(rows,
        containsInAnyOrder(
            contains(is("Liberty Island"), is("stone"), is("4.0"), is("4"), is("24.0")),
            contains(is("Manhattan"), is("brick"), is("0.0"), is("4"), is("0.0")),
            contains(is("Manhattan"), is("steel"), is("9.0"), is("9"), is("45.0")),
            contains(is("Manhattan"), is("stone"), is("6.0"), is("4"), is("60.0")),
            contains(is("Manhattan"), is("timber"), is("0.0"), is("1"), is("0.0")),
            contains(is("Manhattan"), is("concrete"), is("0.0"), is("7"), is("0.0"))
        ));
  }

  @Test
  public void canAggregateByAll() throws Exception {
    // Grouping by a constant (eg the string total) should put everything into one group
    // We use this behaviour in the wizard, so just making sure it still works
    runCommand.modelId = "aggregate-results";
    runCommand.parameters = List.of(
            "report-event-impact.group-by[0] = 'total' as Results",
            "report-event-impact.group-by[1] = "
    );

    runCommand.run();

    List<List<String>> rows = openCsv("event-impact.csv", "Results", "severity", "resilience", "damage");
    assertThat(rows.size(), is(1));
  }

  @Test
  public void canAggregateByConsequence() throws Exception {
    runCommand.modelId = "aggregate-consequence";
    runCommand.run();

    // check model outputs the expected exposed buildings
    List<List<String>> rows = openCsv("event-impact.csv", "building_name", "exposed");
    assertThat(rows,
        containsInAnyOrder(
            contains(equalTo("The Hayden Planetarium"), equalTo("0")),
            contains(equalTo("That Cafe From Seinfeld"), equalTo("0")),
            contains(equalTo("Statue of Liberty"), equalTo("1")),
            contains(equalTo("Natural History Museum"), equalTo("0")),
            contains(equalTo("NYSE"), equalTo("1")),
            contains(equalTo("City Hall"), equalTo("0")),
            contains(equalTo("New York Public Library"), equalTo("0")),
            contains(equalTo("The Plaza Hotel"), equalTo("0"))));
  }

  @Test
  public void canAggregateWithBuckets() throws Exception {
    runCommand.modelId = "aggregate-bucket";
    runCommand.run();

    List<List<String>> rows = openCsv("event-impact.csv",
            "construction", "bucketing.range_<_5.sumStoryDamage", "bucketing.range_5_+.sumStoryDamage");

    assertThat(rows, hasItems(
            contains(equalTo("steel"), equalTo(""), equalTo("45.0")),
            contains(equalTo("brick"), equalTo("0.0"), equalTo("0.0")),
            contains(equalTo("stone"), equalTo("0.0"), equalTo("84.0"))
    ));
  }

  @Test
  public void canAggregateNormallyAndWithBuckets() throws Exception {
    runCommand.modelId = "aggregate-bucket";
    runCommand.parameters = List.of("report-event-impact.aggregate[0] = max(consequence.severity) as severity");

    runCommand.run();

    List<List<String>> rows = openCsv("event-impact.csv",
            "construction", "severity", "bucketing.range_<_5.sumStoryDamage", "bucketing.range_5_+.sumStoryDamage");

    assertThat(rows, hasItems(
            contains(equalTo("steel"), equalTo("9.0"), equalTo(""), equalTo("45.0")),
            contains(equalTo("brick"), equalTo("0.0"), equalTo("0.0"), equalTo("0.0")),
            contains(equalTo("stone"), equalTo("6.0"), equalTo("0.0"), equalTo("84.0"))
    ));
  }

  @Test
  public void canOverrideHazardParameterViaCli() throws Exception {
    runCommand.modelId = "aggregate-consequence";
    // change the hazard from kajiu_route to kaiju_fire
    runCommand.parameters = Arrays.asList("input-hazards.layer=kaiju_fire");
    runCommand.run();

    // check the results have changed around which buildings were exposed
    List<List<String>> rows = openCsv("event-impact.csv", "building_name", "exposed");
    assertThat(rows,
        containsInAnyOrder(
            contains(equalTo("The Hayden Planetarium"), equalTo("0")),
            contains(equalTo("That Cafe From Seinfeld"), equalTo("0")),
            contains(equalTo("Statue of Liberty"), equalTo("1")),
            contains(equalTo("Natural History Museum"), equalTo("1")),
            contains(equalTo("NYSE"), equalTo("0")),
            contains(equalTo("City Hall"), equalTo("0")),
            contains(equalTo("New York Public Library"), equalTo("1")),
            contains(equalTo("The Plaza Hotel"), equalTo("0"))));
  }

  @Test
  public void canOverrideHazardViaCliWithNonBookmarkFile() throws Exception {
    runCommand.modelId = "aggregate-consequence";
    // same as canOverrideHazardParameterViaCli() but point to the file itself
    // (rather than a bookmark)
    runCommand.parameters = Arrays.asList(
        "input-hazards.layer=../../../../../../examples/kaiju/data/kaiju-fire.shp");
    runCommand.run();

    // note that there's a slight difference in whether The Hayden Planetarium is
    // exposed with the raster vs vector data for the fire blast
    List<List<String>> rows = openCsv("event-impact.csv", "building_name", "exposed");
    assertThat(rows,
        containsInAnyOrder(
            contains(equalTo("The Hayden Planetarium"), equalTo("0")),
            contains(equalTo("That Cafe From Seinfeld"), equalTo("0")),
            contains(equalTo("Statue of Liberty"), equalTo("1")),
            contains(equalTo("Natural History Museum"), equalTo("1")),
            contains(equalTo("NYSE"), equalTo("0")),
            contains(equalTo("City Hall"), equalTo("0")),
            contains(equalTo("New York Public Library"), equalTo("1")),
            contains(equalTo("The Plaza Hotel"), equalTo("0"))));
  }

  @Test
  public void canUseAreaLayer() throws Exception {
    // check we can add an area-layer to a non-point model
    runCommand.modelId = "area-layer";
    runCommand.run();

    List<List<String>> rows = openCsv("event-impact.csv", "building_name", "borough");
    assertThat(rows,
        containsInAnyOrder(
            contains(equalTo("Statue of Liberty"), equalTo("Liberty Island")),
            contains(equalTo("Natural History Museum"), equalTo("Manhattan")),
            contains(equalTo("New York Public Library"), equalTo("Manhattan")),
            contains(equalTo("Trump Tower"), equalTo("Manhattan"))));

    // here we also check we get multiple outputs from the same model
    List<List<String>> aggRows = openCsv("area-losses.csv", "name", "sum", "adjustedSum");
    assertThat(aggRows,
        containsInAnyOrder(
            contains(equalTo("Liberty Island"), equalTo("1"), equalTo("1.5")),
            contains(equalTo("Manhattan"), equalTo("3"), equalTo("4.5"))));
  }

  public Matcher<String> isPoint(double x, double y) {
    return GeoHelper.wktGeometryMatch(String.format("POINT (%f %f)", x, y), 0.0001);
  }

  @Test
  public void canUseAreaLayerWhichIsCut() throws Exception {
    // check we make use of an area-layer that is cut. Not sure if this really makes sense to do, but
    // the wizard allows use to do it, so it should work.
    runCommand.modelId = "area-layer-cut";
    runCommand.run();

    List<List<String>> rows = openCsv("event-impact.csv", "building_name", "borough", "borough_segment");
    assertThat(rows,
        containsInAnyOrder(
            contains(equalTo("Statue of Liberty"), equalTo("Liberty Island"), isPoint(-74.0452, 40.6899)),
            contains(equalTo("Natural History Museum"), equalTo("Manhattan"),  isPoint(-73.9773, 40.7773)),
            contains(equalTo("New York Public Library"), equalTo("Manhattan"), isPoint(-73.9777, 40.7503)),
            contains(equalTo("Trump Tower"), equalTo("Manhattan"),             isPoint(-73.9776, 40.7593))));

    // here we also check we get multiple outputs from the same model
    List<List<String>> aggRows = openCsv("area-losses.csv", "name", "segment", "sum", "adjustedSum");
    assertThat(aggRows,
        containsInAnyOrder(
            contains(equalTo("Liberty Island"), isPoint(-74.0452, 40.6899), equalTo("1"), equalTo("1.5")),
            contains(equalTo("Manhattan"), isPoint(-73.9773, 40.7773), equalTo("1"), equalTo("1.5")),
            contains(equalTo("Manhattan"), isPoint(-73.9777, 40.7503), equalTo("1"), equalTo("1.5")),
            contains(equalTo("Manhattan"), isPoint(-73.9776, 40.7593), equalTo("1"), equalTo("1.5"))
    ));
  }

  @Test
  public void canAggregateByHazard() throws Exception {
    runCommand.modelId = "aggregate-hazard";
    runCommand.run();

    List<List<String>> rows = openCsv("event-impact.csv", "building_name", "severity");
    assertThat(rows,
        containsInAnyOrder(
            contains(equalTo("The Hayden Planetarium"), equalTo("2.0")),
            contains(equalTo("Statue of Liberty"), equalTo("4.0")),
            contains(equalTo("Natural History Museum"), equalTo("2.0")),
            contains(equalTo("Trump Tower"), equalTo("9.0")),
            contains(equalTo("New York Public Library"), equalTo("3.0"))));
  }

  @Test
  public void canUseResourceLayer() throws Exception {
    runCommand.modelId = "resource-layer";
    runCommand.run();

    // this is almost the same results as canAggregateByHazard, as it's just using the fire hazard
    // as a resource layer. (Hayden Planetarium is excluded in this case because we're matching
    // the resource layer by centroid rather than any intersection)
    List<List<String>> rows = openCsv("event-impact.csv", "building_name", "resource");
    assertThat(rows,
        containsInAnyOrder(
            contains(equalTo("Statue of Liberty"), equalTo("4.0")),
            contains(equalTo("Natural History Museum"), equalTo("2.0")),
            contains(equalTo("Trump Tower"), equalTo("9.0")),
            contains(equalTo("New York Public Library"), equalTo("3.0"))));
  }

  @Test
  public void canRunModelWithUnansweredQuestionSets() throws Exception {
    runCommand.modelId = "geoprocess-only";
    runCommand.runnerOptions.format = "csv";
    runCommand.run();

    // note different output filename, as we're only running the model up to the exposures phase
    List<List<String>> rows = openCsv("exposures.csv",
        "exposure.geom", "exposure.name", "exposure.stories", "exposure.construction");
    assertThat(rows,
        contains(
            contains(IsAnything.anything(), equalTo("Statue of Liberty"), equalTo("22"), equalTo("stone"))
          ));
  }

  @Test
  public void cannotPartiallyAnswerQuestionSetIfRequiredQuestionsRemain() throws Exception {
    runCommand.modelId = "err-partial-geoprocess";

    // something is going to run
    runCommand.run();

    String errors = render(terminal.getCollectedProblems());
    assertThat(errors, containsString("You are running a saved model where not all wizard questions "
        + "have been answered completely (i.e. 'input-exposures.filter-layer-condition'). "
        + "This may produce model results that are incomplete."));

  }

  @Test
  public void cannotAssignBadParameterTypeInConfig() throws Exception {
    runCommand.modelId = "err-bad-param-type";
    ExitException ex = assertThrows(ExitException.class, () -> runCommand.run());
    Parameter badParam = Parameter.required("input-exposures.layer", ResolvedBookmark.class);
    assertThat(ex.getProblem(), hasAncestorProblem(Matchers.equalIgnoringChildren(
        WizardProblems.get().configError().withChildren(Problems.foundWith(badParam))
    )));
  }

  @Test
  public void cannotSpecifyBadArityInConfig() throws Exception {
    runCommand.modelId = "err-bad-arity";
    ExitException ex = assertThrows(ExitException.class, () -> runCommand.run());
    // we can't specify the exposure-layer twice
    assertThat(ex.getProblem(), hasAncestorProblem(Matchers.equalIgnoringChildren(
        ParamProblems.get().wrongNumberGiven("input-exposures.layer", "[1..1]", 2))));
  }

  @Test
  public void testExposureLayerMustBeSpecified() throws Exception {
    runCommand.modelId = "err-no-params";
    runCommand.run();

    // this one is kinda odd, the command still runs okay, even though there is no pipeline to run.
    String errors = render(terminal.getCollectedProblems());
    assertThat(errors, containsString("You are running a saved model where not all wizard questions "
        + "have been answered completely"));
  }

  @Test
  public void extraneousNonWizardParamsAreIgnored() throws Exception {
    // same as warningForUnknownParameters but bogus param is in project.ini file
    runCommand.modelId = "bogus-param";
    runCommand.run();

    // the wizard just ignores extraneous parameters that don't concern it, so should still run
    List<List<String>> rows = openCsv("event-impact.csv", "building_name");
    assertThat(rows,
        containsInAnyOrder(
            contains(equalTo("Statue of Liberty")),
            contains(equalTo("Natural History Museum")),
            contains(equalTo("New York Public Library")),
            contains(equalTo("Trump Tower"))));

    assertThat(terminal.getCollectedProblems(), contains(hasAncestorProblem(
        equalIgnoringChildren(ParamProblems.get().ignored("[bogus.param]"))
    )));
  }

  @Test
  public void warningsDisplayedWhenExposureLayerHasWarning() throws Exception {
    runCommand.modelId = "bookmark-has-warning";
    runCommand.run();

    List<List<String>> rows = openCsv("event-impact.csv", "building_name");
    assertThat(rows,
        containsInAnyOrder(
            contains(equalTo("Statue of Liberty")),
            contains(equalTo("Natural History Museum")),
            contains(equalTo("New York Public Library")),
            contains(equalTo("Trump Tower"))));

    // warning should relate to the parameter that the user enters
    Parameter paramWithWarning = Parameter.required("input-exposures.layer", ResolvedBookmark.class);
    assertThat(terminal.getCollectedProblems(), contains(hasAncestorProblem(
        equalIgnoringChildren(Problems.foundWith(paramWithWarning).withSeverity(Severity.WARNING))
    )));
  }

  @Ignore // ignored because currently the sampling question is always asked (even for point exposures)
          // so changing to a line/polygon exposure still works (most likely with centroid sampling but
          // that depends how the sample question was answered)
  @Test
  public void nonPointExposureQuestionsMustBeAnswered() throws Exception {
    // check that the wizard logic is preserved and we *must* answer additional
    // questions when using a non-point exposure-layer (which we're overriding onthe
    // CLI in this case)
    runCommand.modelId = "point-assets";
    // does the sample type default to centroid now?
    runCommand.parameters = Arrays.asList("input-exposures.layer=nyc_buildings");

    // changing the exposure-layer to non-point now means we need to answer the
    // 'sample' question-set as well
    Parameter requiredParam = Parameter.required("sample.by-geometry", SampleType.class);
    ExitException ex = assertThrows(ExitException.class, () -> runCommand.run());
    assertThat(ex.getProblem(), hasAncestorProblem(equalTo(
        GeneralProblems.get().required(requiredParam)
    )));
  }

  @Test
  public void cannotAnswerNonPointQuestionsWithPointBasedExposures() throws Exception {
    // flip-side of the previous test - we have a point-based exposure-layer and we
    // incorrectly try to answer non-point questions
    runCommand.modelId = "point-assets";
    runCommand.parameters = Arrays.asList("sample.by-geometry=ALL_INTERSECTIONS");

    runCommand.run();

    assertThat(
      render(terminal.getCollectedProblems()),
      containsString("Surplus parameters were given that were ignored: [sample.by-geometry]")
    );
  }

  @Test
  public void modelWithUnrealizableAnswersIsHandledNicely() throws Exception {
    runCommand.modelId = "err-unrealizable";
    // this model has a bad select expression: exposure.name + 1
    ExitException ex = Assert.assertThrows(ExitException.class, () -> runCommand.run());
    assertThat(ex.getProblem(), hasAncestorProblem(Matchers.equalIgnoringChildren(
          ExpressionProblems.get().noSuchOperatorFunction("+", Arrays.asList(Types.TEXT, Types.INTEGER))
    )));
  }
}
