var CS = ConstraintSolver;

// "Input tests" are the new style of tests that operate by creating a
// CS.Input (representing a problem statement) and passing it into
// CS.PackagesResolver.
// For tests of CS.Input serialization, see constraint-solver-tests.js.


// Yeah, we rely on object key order here.  Specifically that
// if you add a bunch of keys to an object (that look like package
// names) and then JSON.stringify the object, the keys will appear
// in that order.  If that's not true, these tests will break.
var sortKeys = function (obj) {
  var result = {};
  Object.keys(obj).sort().forEach(function (k) {
    result[k] = obj[k];
  });
  return result;
};
var formatSolution = function (obj) {
  // Note that we use JSON so that it's easy to copy-and-paste test
  // results into tests.
  return JSON.stringify({
    answer: sortKeys(obj.answer),
    allAnswers: obj.allAnswers && obj.allAnswers.map(sortKeys),
    neededToUseUnanticipatedPrereleases: obj.neededToUseUnanticipatedPrereleases
  }, null, 2);
};

var doTest = async function (test, inputJSONable, outputJSONable, options) {
  var input;
  if (inputJSONable instanceof CS.Input) {
    input = inputJSONable;
  } else {
    input = CS.Input.fromJSONable(inputJSONable);
  }

  if (typeof outputJSONable.neededToUseUnanticipatedPrereleases !== 'boolean') {
    outputJSONable = Object.assign(outputJSONable, {
      neededToUseUnanticipatedPrereleases: (
        !! outputJSONable.neededToUseUnanticipatedPrereleases)
    });
  }

  test.equal(
    formatSolution(await CS.PackagesResolver._resolveWithInput(input, options)),
    formatSolution(outputJSONable));
};

var doFailTest = async function (test, inputJSONable, messageExpect) {
  var input;
  if (inputJSONable instanceof CS.Input) {
    input = inputJSONable;
  } else {
    input = CS.Input.fromJSONable(inputJSONable);
  }

  await test.throwsAsync(async function () {
    try {
      await CS.PackagesResolver._resolveWithInput(input);
    } catch (e) {
      if (! e.constraintSolverError) {
        test.fail(e.message);
      }
      throw e;
    }
  }, messageExpect);
};

Tinytest.addAsync("constraint solver - input - upgrade indirect dependency", async function (test) {
  await doTest(test, {
    dependencies: ["foo"],
    constraints: [],
    previousSolution: { foo: "1.0.0", bar: "2.0.0" },
    upgrade: ["bar"],
    catalogCache: {
      data: {
        "foo 1.0.0": ["bar@2.0.0"],
        "bar 2.0.0": [],
        "bar 2.0.1": []
      }
    }
  }, {
    // if you specify an indirect dependency in `meteor update`,
    // it will get bumped up to a newer version
    answer: {
      foo: "1.0.0",
      bar: "2.0.1"
    }
  });
});

Tinytest.addAsync("constraint solver - input - upgrade direct, don't break", async function (test) {
  await doTest(test, {
    dependencies: ["foo", "bar"],
    constraints: [],
    previousSolution: { foo: "1.0.0", bar: "2.0.0" },
    upgrade: ["bar"],
    catalogCache: {
      data: {
        "foo 1.0.0": ["bar@2.0.0||3.0.0"],
        "bar 2.0.0": [],
        "bar 3.0.0": []
      }
    }
  }, {
    answer: {
      foo: "1.0.0",
      bar: "2.0.0"
    }
  });

  // if allowIncompatibleUpdate is set, upgrade bar to 3.0.0
  await doTest(test, {
    dependencies: ["foo", "bar"],
    constraints: [],
    previousSolution: { foo: "1.0.0", bar: "2.0.0" },
    upgrade: ["bar"],
    allowIncompatibleUpdate: true,
    catalogCache: {
      data: {
        "foo 1.0.0": ["bar@2.0.0||3.0.0"],
        "bar 2.0.0": [],
        "bar 3.0.0": []
      }
    }
  }, {
    answer: {
      foo: "1.0.0",
      bar: "3.0.0"
    }
  });
});

Tinytest.addAsync("constraint solver - input - previous solution no patch", async function (test) {
  await doTest(test, {
    dependencies: ["foo"],
    constraints: [],
    previousSolution: { foo: "1.0.0", bar: "2.0.0" },
    catalogCache: {
      data: {
        "foo 1.0.0": ["bar@2.0.0"],
        "foo 1.0.1": ["bar@2.0.1"],
        "bar 2.0.0": [],
        "bar 2.0.1": []
      }
    }
  }, {
    answer: {
      foo: "1.0.0",
      bar: "2.0.0"
    }
  });
});


Tinytest.addAsync("constraint solver - input - don't break root dep", async function (test) {
  await doTest(test, {
    dependencies: ["foo", "bar"],
    constraints: [],
    previousSolution: { bar: "2.0.0" },
    upgrade: [],
    catalogCache: {
      data: {
        "foo 1.0.0": ["bar@=2.0.1"],
        "bar 2.0.0": [],
        "bar 2.0.1": []
      }
    }
  }, {
    answer: {
      foo: "1.0.0",
      bar: "2.0.1"
    }
  });

  await doFailTest(test, {
    dependencies: ["foo", "bar"],
    constraints: [],
    previousSolution: { bar: "2.0.1" },
    upgrade: [],
    catalogCache: {
      data: {
        "foo 1.0.0": ["bar@=2.0.0"],
        "bar 2.0.0": [],
        "bar 2.0.1": []
      }
    }
  }, 'Potentially incompatible change required to top-level dependency: bar 2.0.0, was 2.0.1.\nConstraints on package "bar":\n* bar@=2.0.0 <- foo 1.0.0\n\nTo allow potentially incompatible changes to top-level dependencies, you must pass --allow-incompatible-update on the command line.');
});

Tinytest.addAsync("constraint solver - input - don't pick RCs", async function (test) {
  // First verify that the solver takes the latest version when
  // presented with a new root dependency (i.e. one not mentioned in
  // a previous solution) -- and also that it will take a prerelease if
  // it has no choice.
  await doTest(test, {
    dependencies: ["a"],
    constraints: [],
    catalogCache: {
      data: {
        "a 1.0.0-pre.0": [],
        "a 1.0.0-pre.1": []
      }
    }
  }, {
    answer: {
      a: "1.0.0-pre.1"
    },
    neededToUseUnanticipatedPrereleases: true
  });

  // If we have the option to take a non-pre-release version,
  // we should.
  await doTest(test, {
    dependencies: ["a"],
    constraints: [],
    catalogCache: {
      data: {
        "a 0.9.0": [],
        "a 1.0.0-pre.0": [],
        "a 1.0.0-pre.1": []
      }
    }
  }, {
    answer: {
      a: "0.9.0"
    },
    neededToUseUnanticipatedPrereleases: false
  });

  // If the prerelease versions are "anticipated", take
  // the latest one.
  await doTest(test, {
    dependencies: ["a"],
    constraints: [],
    anticipatedPrereleases: { a: {
      "1.0.0-pre.0": true, "1.0.0-pre.1": true } },
    catalogCache: {
      data: {
        "a 0.9.0": [],
        "a 1.0.0-pre.0": [],
        "a 1.0.0-pre.1": []
      }
    }
  }, {
    answer: {
      a: "1.0.0-pre.1"
    },
    neededToUseUnanticipatedPrereleases: false
  });

  // Don't take the unanticipated pre-releases here.
  await doTest(test, {
    dependencies: ["a"],
    constraints: ["a@1.0.0"],
    catalogCache: {
      data: {
        "a 1.0.0": [],
        "a 1.0.1-pre.0": [],
        "a 1.0.1-pre.1": []
      }
    }
  }, {
    answer: {
      a: "1.0.0"
    },
    neededToUseUnanticipatedPrereleases: false
  });

  // If we ask for one prerelease, we might get another.
  // If it isn't anticipated, it sets the flag on the result
  // (differs from older behavior).
  await doTest(test, {
    dependencies: ["a"],
    constraints: ["a@1.0.1-pre.0"],
    anticipatedPrereleases: { a: {
      "1.0.0-pre.0": true } },
    catalogCache: {
      data: {
        "a 1.0.0": [],
        "a 1.0.1-pre.0": [],
        "a 1.0.1-pre.1": []
      }
    }
  }, {
    answer: {
      a: "1.0.1-pre.1"
    },
    neededToUseUnanticipatedPrereleases: true
  });

});

Tinytest.addAsync("constraint solver - input - previous solution no longer needed", async function (test) {
  await doTest(test, {
    dependencies: ["foo"],
    constraints: [],
    previousSolution: { foo: "1.0.0", bar: "1.0.0" },
    catalogCache: {
      data: {
        "foo 0.1.0": ["bar@1.0.0"],
        "foo 1.0.0": [],
        "bar 1.0.0": []
      }
    }
  }, {
    answer: {
      foo: "1.0.0"
    }
  });
});

Tinytest.addAsync("constraint solver - input - conflicting top-level constraints", async function (test) {
  // conflicting dependencies don't matter if we don't need the package
  await doTest(test, {
    dependencies: [],
    constraints: ["foo@1.0.0", "foo@2.0.0"],
    previousSolution: {},
    catalogCache: {
      data: {
        "foo 1.0.0": [],
        "foo 2.0.0": []
      }
    }
  }, {
    answer: {
    }
  });

  // ... but they do if we do
  await doFailTest(test, {
    dependencies: ["bar"],
    constraints: ["foo@1.0.0", "foo@2.0.0"],
    previousSolution: {},
    catalogCache: {
      data: {
        "foo 1.0.0": [],
        "foo 2.0.0": [],
        "bar 1.0.0": ["foo"]
      }
    }
  }, /No version of foo satisfies all constraints: @1.0.0, @2.0.0/);
});

Tinytest.addAsync("constraint solver - input - unknown package", async function (test) {
  await doFailTest(test, {
    dependencies: ["bar"],
    constraints: [],
    catalogCache: {
      data: {}
    }
  }, /unknown package in top-level dependencies: bar/);
  await doFailTest(test, {
    dependencies: ['foo'],
    constraints: [],
    catalogCache: {
      data: {
        "foo 1.0.0": ["bar"]
      }
    }
  }, /unknown package: bar\nRequired by: foo 1.0.0/);
  await doFailTest(test, {
    dependencies: ["isobuild:bar"],
    constraints: [],
    catalogCache: {
      data: {}
    }
  }, /unsupported Isobuild feature "isobuild:bar" in top-level dependencies/);
  await doFailTest(test, {
    dependencies: ['foo'],
    constraints: [],
    catalogCache: {
      data: {
        "foo 1.0.0": ["isobuild:bar"]
      }
    }
  }, /unsupported Isobuild feature "isobuild:bar";.*\nRequired by: foo 1.0.0/);
});

Tinytest.addAsync("constraint solver - input - previous indirect deps", async function (test) {
  await doTest(test, {
    dependencies: ["a"],
    constraints: [],
    previousSolution: { c: "1.2.3" },
    catalogCache: {
      data: {
        "a 1.0.0": ["b"],
        "b 1.0.0": ["c"],
        "c 1.0.0": [],
        "c 1.2.2": [],
        "c 1.2.3": [],
        "c 1.2.4": [],
        "c 1.3.0": [],
        "c 2.0.0": []
      }
    }
  }, {
    answer: {
      a: "1.0.0",
      b: "1.0.0",
      c: "1.2.3" // take same version as in previous solution
    }
  });
});

Tinytest.addAsync("constraint solver - input - new indirect deps", async function (test) {
  await doTest(test, {
    dependencies: ["a"],
    constraints: [],
    previousSolution: {},
    catalogCache: {
      data: {
        "a 1.0.0": ["b"],
        "b 1.0.0": ["c@1.2.0"],
        "c 1.0.0": [],
        "c 1.2.2": [],
        "c 1.2.3": [],
        "c 1.2.4": [],
        "c 1.3.0": [],
        "c 2.0.0": []
      }
    }
  }, {
    answer: {
      a: "1.0.0",
      b: "1.0.0",
      c: "1.2.4" // take patches only (use oldest major/minor possible)
    }
  });
});

Tinytest.addAsync("constraint solver - input - trade-off", async function (test) {
  await doTest(test, {
    dependencies: ["a"],
    constraints: [],
    previousSolution: { b: "1.0.0", c: "1.0.0" },
    catalogCache: {
      data: {
        "a 1.0.0": ["b", "c"],
        "b 1.0.0": ["x"],
        "b 1.0.1": ["y@1.0.0"],
        "b 1.0.2": ["y@2.0.0"],
        "c 1.0.0": ["x"],
        "c 1.0.1": ["y@2.0.0"],
        "c 1.0.2": ["x"],
        "c 1.0.3": ["y@1.0.0"],
        "y 1.0.0": [],
        "y 2.0.0": []
      }
    }
  }, {
    // given a choice between (b,c) being (1.0.1, 1.0.3) or (1.0.2, 1.0.1),
    // the latter should be preferred; indirect dependencies with a previous
    // solution should be jointly made as old as possible.
    answer: {
      a: "1.0.0",
      b: "1.0.2",
      c: "1.0.1",
      y: "2.0.0"
    }
  });

  await doTest(test, {
    dependencies: ["a", "b", "c"],
    constraints: [],
    previousSolution: {},
    catalogCache: {
      data: {
        "a 1.0.0": ["b", "c"],
        "b 1.0.0": ["x"],
        "b 1.0.1": ["y@1.0.0"],
        "b 1.0.2": ["y@2.0.0"],
        "c 1.0.0": ["x"],
        "c 1.0.1": ["y@2.0.0"],
        "c 1.0.2": ["x"],
        "c 1.0.3": ["y@1.0.0"],
        "y 1.0.0": [],
        "y 2.0.0": []
      }
    }
  }, {
    // now we should prefer "b" and "c" to jointly be as new as possible,
    // because they are direct dependencies with no previous solution.
    answer: {
      a: "1.0.0",
      b: "1.0.1",
      c: "1.0.3",
      y: "1.0.0"
    }
  });

});

Tinytest.addAsync("constraint solver - input - fake PackageConstraint", async function (test) {
  // The tool gives us PackageConstraint objects constructed with a different
  // copy of package-version-parser.  If we're not careful in CS.Input or
  // CS.Solver, this case will throw an error.  See comments in CS.Input.
  var fakeConstraint = new (function () {});
  fakeConstraint.package = 'foo';
  fakeConstraint.constraintString = '2.0.0';
  fakeConstraint.toString = function () {
    return 'foo@2.0.0';
  };
  fakeConstraint.versionConstraint = new (function () {});
  fakeConstraint.versionConstraint.raw = '2.0.0';
  fakeConstraint.versionConstraint.alternatives = [
    {type: 'compatible-with', versionString: '2.0.0' }
  ];
  fakeConstraint.versionConstraint.toString = function () {
    return '2.0.0';
  };

  await doFailTest(test,
             new CS.Input(["foo", "bar"], [fakeConstraint],
                          CS.CatalogCache.fromJSONable({
                            data: {
                              "foo 1.0.0": [],
                              "foo 2.0.0": [],
                              "bar 1.0.0": ["foo@1.0.0"]
                            }
                          })),
             /Constraint foo@1.0.0 is not satisfied by foo 2.0.0/);
});

Tinytest.addAsync("constraint solver - input - stack overflow bug", async function (test) {
  // This case is taken from the "solver-error" branch of the meteor/rectangles
  // repo.  It's an app running from a release (new-version-solver-2) with an
  // unsatisfiable constraint in .meteor/packages.  It caused a stack overflow
  // before logic-solver got smarter about avoiding recursion in formula
  // generation, and it also tests the case where an unsatisfiable constraint is
  // in .meteor/packages.
  //
  // It's not actually a good test of logic-solver overflowing the stack anymore,
  // because the constraint-solver is smarter now.
  await doFailTest(test, STACK_OVERFLOW_BUG_INPUT,
             /No version of follower-livedata satisfies all constraints: @0.9.0/);
});


Tinytest.add("constraint solver - input - bad package name", function (test) {
  test.throws(function () {
    new CS.Input(['-x'], [], new CS.CatalogCache());
  }, /may not begin with a hyphen/);

  test.throws(function () {
    new CS.Input([], [], new CS.CatalogCache(),
                 { previousSolution: { $a: '1.0.0' } });
  }, /Package names can only contain/);

  test.throws(function () {
    new CS.Input([], [], new CS.CatalogCache(),
                 { upgrade: ['$a'] });
  }, /Package names can only contain/);

  test.throws(function () {
    new CS.Input([], [], new CS.CatalogCache(),
                 { upgrade: ['-a'] });
  }, /may not begin with a hyphen/);
});


Tinytest.addAsync("constraint solver - input - slow solve", async function (test) {
  var input = CS.Input.fromJSONable(SLOW_TEST_DATA);

  test.equal(
    formatSolution(await CS.PackagesResolver._resolveWithInput(input)),
    formatSolution({
      "answer":{
        "autopublish":"1.0.2",
        "u2622:persistent-session":"0.2.1",
        "blaze":"2.0.4",
        "random":"1.0.2",
        "mobile-status-bar":"1.0.2",
        "deps":"1.0.6",
        "follower-livedata":"1.0.3",
        "spacebars":"1.0.4",
        "spacebars-compiler":"1.0.4",
        "launch-screen":"1.0.1",
        "iron:location":"1.0.6",
        "http":"1.0.9",
        "json":"1.0.2",
        "check":"1.0.3",
        "retry":"1.0.2",
        "id-map":"1.0.2",
        "reactive-dict":"1.0.5",
        "mrt:moment":"2.8.1",
        "callback-hook":"1.0.2",
        "meteor":"1.1.4",
        "fastclick":"1.0.2",
        "minifiers":"1.1.3",
        "mrt:jquery-ui-sortable":"1.10.3",
        "webapp":"1.1.5",
        "ejson":"1.0.5",
        "skinnygeek1010:parse-form":"0.2.1",
        "iron:controller":"1.0.6",
        "base64":"1.0.2",
        "url":"1.0.3",
        "blaze-tools":"1.0.2",
        "ddp":"1.0.13",
        "iron:core":"1.0.6",
        "splendido:accounts-templates-semantic-ui":"0.0.4",
        "observe-sequence":"1.0.4",
        "reactive-var":"1.0.4",
        "webapp-hashing":"1.0.2",
        "mongo":"1.0.11",
        "htmljs":"1.0.3",
        "ui":"1.0.5",
        "amplify":"1.0.0",
        "meteor-platform":"1.2.1",
        "ordered-dict":"1.0.2",
        "session":"1.0.5",
        "livedata":"1.0.12",
        "templating":"1.0.10",
        "binary-heap":"1.0.2",
        "mizzao:timesync":"0.2.2",
        "tracker":"1.0.4",
        "autoupdate":"1.1.4",
        "html-tools":"1.0.3",
        "reload":"1.1.2",
        "less":"1.0.12",
        "application-configuration":"1.0.4",
        "gfk:notifications":"1.1.1",
        "underscore":"1.0.2",
        "iron:dynamic-template":"1.0.6",
        "routepolicy":"1.0.3",
        "iron:router":"1.0.6",
        "insecure":"1.0.2",
        "iron:layout":"1.0.6",
        "geojson-utils":"1.0.2",
        "minimongo":"1.0.6",
        "iron:url":"1.0.6",
        "jquery":"1.11.2",
        "boilerplate-generator":"1.0.2",
        "iron:middleware-stack":"1.0.6",
        "logging":"1.0.6"
      },
      "neededToUseUnanticipatedPrereleases":false
    }));
});

Tinytest.addAsync("constraint solver - input - update unknown", async function (test) {
  // trying to update an unknown package is currently not an error
  // at the CS.Input level.  It IS an error in the tool, so this case
  // won't make it through in actual tool usage.
  await doTest(test, {
    dependencies: ["direct"],
    constraints: [],
    previousSolution: {
      direct: "1.0.0"
    },
    upgrade: ["unknown"],
    catalogCache: {
      data: {
        "direct 1.0.0": []
      }
    }
  }, {
    answer: {
      direct: "1.0.0"
    }
  });
});

Tinytest.addAsync("constraint solver - input - update indirect deps", async function (test) {
  // test upgrading an indirect dependency explicitly.
  // `meteor update indirect` takes it from 1.0.0 to 2.0.0 (with no concern
  // about bumping the major version because it's not a top-level package,
  // and the only package that uses it doesn't specify any constraint).
  await doTest(test, {
    dependencies: ["direct"],
    constraints: [],
    previousSolution: {
      direct: "1.0.0",
      indirect: "1.0.0"
    },
    upgrade: ["indirect"],
    catalogCache: {
      data: {
        "direct 1.0.0": ["indirect"],
        "indirect 1.0.0": [],
        "indirect 2.0.0": []
      }
    }
  }, {
    answer: {
      direct: "1.0.0",
      indirect: "2.0.0"
    }
  });

  // Normally, we don't take patches to indirect dependencies, even when
  // updating direct dependencies.  This is what would happen if the user
  // typed `meteor update direct`.
  await doTest(test, {
    dependencies: ["direct"],
    constraints: [],
    previousSolution: {
      direct: "1.0.0",
      indirect: "1.0.0"
    },
    upgrade: ["direct"],
    catalogCache: {
      data: {
        "direct 1.0.0": ["indirect"],
        "direct 1.5.0": ["indirect"],
        "direct 2.0.0": ["indirect"],
        "indirect 1.0.0": [],
        "indirect 1.0.1": [],
        "indirect 1.1.0": []
      }
    }
  }, {
    answer: {
      direct: "1.5.0", // upgraded (but not to higher major version)
      indirect: "1.0.0" // not upgraded
    }
  });

  // If upgradeIndirectDepPatchVersions is true, user just typed
  // `meteor update`.  Take the opportunity to take patches to indirect
  // dependencies.
  await doTest(test, {
    dependencies: ["direct"],
    constraints: [],
    previousSolution: {
      direct: "1.0.0",
      indirect: "1.0.0",
      indirect2: "1.0.0"
    },
    upgrade: ["direct"],
    upgradeIndirectDepPatchVersions: true,
    catalogCache: {
      data: {
        "direct 1.0.0": ["indirect"],
        "direct 1.5.0": ["indirect", "indirect2"],
        "direct 2.0.0": ["indirect"],
        "indirect 1.0.0": [],
        "indirect 1.0.0_1": [],
        "indirect 1.0.1": [],
        "indirect 1.1.0": [],
        "indirect2 1.0.0": [],
        "indirect2 1.0.0_1": [],
        "indirect2 1.0.0_2": [],
        "indirect2 1.1.0": []
      }
    }
  }, {
    answer: {
      direct: "1.5.0", // upgraded (but not to higher major version)
      indirect: "1.0.1", // patch/wrapNum upgraded to latest
      indirect2: "1.0.0_2" // patch/wrapNum upgraded to latest
    }
  });
});

Tinytest.addAsync("constraint solver - input - package is only weak dep", async function (test) {
  await doTest(test, {
    dependencies: ["foo"],
    constraints: [],
    previousSolution: {},
    catalogCache: {
      data: {
        "foo 1.0.0": ["?bar"]
      }
    }
  }, {
    answer: {
      foo: "1.0.0"
    }
  });
});
