/*
 * Copyright 1997-2025 Optimatika
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.ojalgo.optimisation.integer;

import org.junit.jupiter.api.Test;
import org.ojalgo.TestUtils;
import org.ojalgo.netio.BasicLogger;
import org.ojalgo.optimisation.ExpressionsBasedModel;
import org.ojalgo.optimisation.ModelFileTest;
import org.ojalgo.optimisation.Optimisation;
import org.ojalgo.optimisation.Optimisation.Result;
import org.ojalgo.optimisation.OptimisationCase;
import org.ojalgo.optimisation.Variable;
import org.ojalgo.type.context.NumberContext;

/**
 * MIP models with a known solution. Tests verify that the generated cuts do not cut off the known optimal
 * solution. Primarily of interest while developing cut generation features.
 *
 * @author apete
 */
public class GeneratedCutTest extends OptimisationIntegerTests implements ModelFileTest {

    private static final NumberContext ACCURACY = NumberContext.of(8);

    private static void doTest(final ExpressionsBasedModel model, final Optimisation.Result minSolution, final Optimisation.Result maxSolution) {

        model.options.validate = true; // This is what turns on validation

        if (DEBUG) {
            // model.options.debug(Optimisation.Solver.class);
            model.options.debug(IntegerSolver.class);
            // model.options.debug(ConvexSolver.class);
            // model.options.debug(LinearSolver.class);
            // model.options.progress(IntegerSolver.class);
            // model.options.validate = false;
            // model.options.integer(IntegerStrategy.DEFAULT.withGapTolerance(NumberContext.of(3)));

            model.options.validate = true;
        }

        if (minSolution != null) {

            TestUtils.assertSolutionValid(model, minSolution, ACCURACY);

            //            model.setKnownSolution(minSolution, (m, s) -> {
            //                if (!DEBUG) {
            //                    BasicLogger.error(s);
            //                    BasicLogger.error(m);
            //                    throw new AssertionFailedError();
            //                }
            //            });

            Result result = model.minimise();

            if (DEBUG) {
                BasicLogger.debug(minSolution);
                BasicLogger.debug(result);
            }

            TestUtils.assertSolutionValid(model, result, ACCURACY);
            TestUtils.assertResult(minSolution, result, ACCURACY);
        }

        if (maxSolution != null) {

            TestUtils.assertSolutionValid(model, maxSolution, ACCURACY);

            //            model.setKnownSolution(maxSolution, (m, s) -> {
            //                if (!DEBUG) {
            //                    BasicLogger.error(s);
            //                    BasicLogger.error(m);
            //                    throw new AssertionFailedError();
            //                }
            //            });

            Result result = model.maximise();

            if (DEBUG) {
                BasicLogger.debug(maxSolution);
                BasicLogger.debug(result);
            }

            TestUtils.assertSolutionValid(model, result, ACCURACY);
            TestUtils.assertResult(maxSolution, result, ACCURACY);
        }
    }

    /**
     * MIP with non-zero lower bounds (shifted variables) solved with both the newer dual SimplexSolver and
     * the older primal SimplexTableauSolver, verifying that GMI cuts are actually generated and that both
     * solvers produce the correct optimum. Uses a model known to need cuts for proof of optimality.
     */
    @Test
    public void testCutGenerationWithShiftedVariables() {

        // Based on the branch-and-cut example but with shifted (non-zero) lower bounds
        ExpressionsBasedModel model = new ExpressionsBasedModel();

        Variable x1 = model.newVariable("x1").integer(true).lower(1).weight(-6);
        Variable x2 = model.newVariable("x2").integer(true).lower(1).weight(-5);

        model.addExpression().upper(11).set(x1, 3).set(x2, 1);
        model.addExpression().upper(5).set(x1, -1).set(x2, 2);

        // Known MIP optimum: x1=3, x2=2 => -28
        Result expected = Result.of(-28, Optimisation.State.OPTIMAL, 3, 2);

        // Solve with newer dual SimplexSolver
        ExpressionsBasedModel dualModel = model.copy();
        dualModel.options.linear().dual();
        dualModel.options.validate = true;

        Result dualResult = dualModel.minimise();
        TestUtils.assertStateNotLessThanOptimal(dualResult);
        TestUtils.assertResult(expected, dualResult, ACCURACY);

        // Solve with older primal SimplexTableauSolver
        ExpressionsBasedModel primalModel = model.copy();
        primalModel.options.linear().primal();
        primalModel.options.validate = true;

        Result primalResult = primalModel.minimise();
        TestUtils.assertStateNotLessThanOptimal(primalResult);
        TestUtils.assertResult(expected, primalResult, ACCURACY);
    }

    @Test
    public void testFacilityLocationCase() {

        OptimisationCase testCase = DesignCase.makeFacilityLocationCase();

        GeneratedCutTest.doTest(testCase.model, null, testCase.result);
    }

    /**
     * Verify cut generation with a model that has variables with non-zero lower bounds. The GomorySolver
     * always uses cuts rather than branching, so it reliably exercises the cut generation path. Combined with
     * non-zero lower bounds this verifies that shift handling in cut generation is correct.
     */
    @Test
    public void testGomoryCutsWithShiftedBounds() {

        ExpressionsBasedModel model = new ExpressionsBasedModel();

        Variable x1 = model.newVariable("x1").integer(true).lower(1).weight(-6);
        Variable x2 = model.newVariable("x2").integer(true).lower(1).weight(-5);

        model.addExpression().upper(11).set(x1, 3).set(x2, 1);
        model.addExpression().upper(5).set(x1, -1).set(x2, 2);

        Result expected = Result.of(-28, Optimisation.State.OPTIMAL, 3, 2);

        GomorySolver solver = new GomorySolver(model);
        Result result = solver.solve();
        TestUtils.assertStateAndSolution(expected, result, NumberContext.of(11));
    }

    @Test
    public void testGr4x6() {

        String minStr = "OPTIMAL 202.35 @ { 20, 0, 25, 0, 0, 0, 0, 30, 0, 0, 0, 5, 15, 0, 0, 0, 5, 0, 0, 0, 0, 15, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0 }";
        Optimisation.Result minSolution = Optimisation.Result.parse(minStr);
        Optimisation.Result maxSolution = null;

        ExpressionsBasedModel model = ModelFileTest.makeModel("miplib", "gr4x6.mps", false);

        GeneratedCutTest.doTest(model, minSolution, maxSolution);
    }

    @Test
    public void testKnapsackCase0() {

        OptimisationCase testCase = KnapsackTest.makeCase0();

        GeneratedCutTest.doTest(testCase.model, null, testCase.result);
    }

    @Test
    public void testKnapsackCase1() {

        OptimisationCase testCase = KnapsackTest.makeCase1();

        GeneratedCutTest.doTest(testCase.model, null, testCase.result);
    }

    @Test
    public void testKnapsackCase2() {

        OptimisationCase testCase = KnapsackTest.makeCase2();

        GeneratedCutTest.doTest(testCase.model, null, testCase.result);
    }

    @Test
    public void testKnapsackCase3() {

        OptimisationCase testCase = KnapsackTest.makeCase3();

        GeneratedCutTest.doTest(testCase.model, null, testCase.result);
    }

    @Test
    public void testKnapsackCase4() {

        OptimisationCase testCase = KnapsackTest.makeCase4();

        GeneratedCutTest.doTest(testCase.model, null, testCase.result);
    }

}
