Index: /trunk/test/unit/org/openstreetmap/josm/gui/preferences/plugin/PluginPreferenceHighLevelTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/gui/preferences/plugin/PluginPreferenceHighLevelTest.java	(revision 14212)
+++ /trunk/test/unit/org/openstreetmap/josm/gui/preferences/plugin/PluginPreferenceHighLevelTest.java	(revision 14213)
@@ -804,5 +804,4 @@
 
         assertEquals(1, jopsMocker.getInvocationLog().size());
-        org.openstreetmap.josm.tools.Logging.error(jopsMocker.getInvocationLog().get(0)[0].toString());
         Object[] invocationLogEntry = jopsMocker.getInvocationLog().get(0);
         assertEquals(JOptionPane.OK_OPTION, (int) invocationLogEntry[0]);
@@ -818,4 +817,9 @@
         this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/dummy_plugin.v31772.jar")));
 
+        // the dummy_plugin jar has been installed
+        TestUtils.assertFileContentsEqual(this.referenceDummyJarNew, this.targetDummyJar);
+        // the baz_plugin jar has not
+        assertFalse(this.targetBazJar.exists());
+
         // loadPlugins(...) was called (with expected parameters)
         assertTrue(loadPluginsCalled[0]);
@@ -827,3 +831,123 @@
         assertEquals("999", Config.getPref().get("pluginmanager.lastupdate", "111"));
     }
+
+    /**
+     * Tests installing a single plugin which has multiple versions advertised, with our JOSM version
+     * preventing us from using the latest version
+     * @throws Exception on failure
+     */
+    @JOSMTestRules.OverrideAssumeRevision("Revision: 7000\n")
+    @Test
+    public void testInstallMultiVersion() throws Exception {
+        TestUtils.assumeWorkingJMockit();
+
+        final String bazOldServePath = "/baz/old.jar";
+        final PluginServer pluginServer = new PluginServer(
+            new PluginServer.RemotePlugin(this.referenceDummyJarNew),
+            new PluginServer.RemotePlugin(this.referenceBazJarNew, ImmutableMap.of(
+                "6800_Plugin-Url", "6;http://localhost:" + this.pluginServerRule.port() + bazOldServePath
+            ))
+        );
+        pluginServer.applyToWireMockServer(this.pluginServerRule);
+        // need to actually serve this older jar from somewhere
+        this.pluginServerRule.stubFor(
+            WireMock.get(WireMock.urlEqualTo(bazOldServePath)).willReturn(
+                WireMock.aResponse().withStatus(200).withHeader("Content-Type", "application/java-archive").withBodyFile(
+                    "plugin/baz_plugin.v6.jar"
+                )
+            )
+        );
+        Config.getPref().putList("plugins", ImmutableList.of());
+
+        final HelpAwareOptionPaneMocker haMocker = new HelpAwareOptionPaneMocker(ImmutableMap.of(
+            "<html>The following plugin has been downloaded <strong>successfully</strong>:"
+            + "<ul><li>baz_plugin (6)</li></ul>"
+            + "You have to restart JOSM for some settings to take effect.<br/><br/>"
+            + "Would you like to restart now?</html>",
+            "Cancel"
+        ));
+        final JOptionPaneSimpleMocker jopsMocker = new JOptionPaneSimpleMocker();
+
+        final PreferenceTabbedPane tabbedPane = new PreferenceTabbedPane();
+
+        tabbedPane.buildGui();
+        // PluginPreference is already added to PreferenceTabbedPane by default
+        tabbedPane.selectTabByPref(PluginPreference.class);
+
+        GuiHelper.runInEDTAndWait(
+            () -> ((javax.swing.JButton) TestUtils.getComponentByName(tabbedPane, "downloadListButton")).doClick()
+        );
+
+        TestUtils.syncEDTAndWorkerThreads();
+
+        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
+        WireMock.resetAllRequests();
+
+        final PluginPreferencesModel model = (PluginPreferencesModel) TestUtils.getPrivateField(
+            tabbedPane.getPluginPreference(),
+            "model"
+        );
+
+        assertTrue(model.getNewlyActivatedPlugins().isEmpty());
+        assertTrue(model.getNewlyDeactivatedPlugins().isEmpty());
+        assertTrue(model.getPluginsScheduledForUpdateOrDownload().isEmpty());
+        assertEquals(model.getDisplayedPlugins(), model.getAvailablePlugins());
+
+        assertEquals(
+            ImmutableList.of("baz_plugin", "dummy_plugin"),
+            model.getAvailablePlugins().stream().map((pi) -> pi.getName()).collect(ImmutableList.toImmutableList())
+        );
+        assertTrue(model.getSelectedPlugins().isEmpty());
+        assertEquals(
+            ImmutableList.of("(null)", "(null)"),
+            model.getAvailablePlugins().stream().map(
+                (pi) -> pi.localversion == null ? "(null)" : pi.localversion
+            ).collect(ImmutableList.toImmutableList())
+        );
+        assertEquals(
+            ImmutableList.of("6", "31772"),
+            model.getAvailablePlugins().stream().map((pi) -> pi.version).collect(ImmutableList.toImmutableList())
+        );
+
+        // now we select dummy_plugin
+        model.setPluginSelected("baz_plugin", true);
+
+        // model should now reflect this
+        assertEquals(
+            ImmutableList.of("baz_plugin"),
+            model.getNewlyActivatedPlugins().stream().map(
+                pi -> pi.getName()
+            ).collect(ImmutableList.toImmutableList())
+        );
+        assertTrue(model.getNewlyDeactivatedPlugins().isEmpty());
+
+        tabbedPane.savePreferences();
+
+        TestUtils.syncEDTAndWorkerThreads();
+
+        assertEquals(1, haMocker.getInvocationLog().size());
+        Object[] invocationLogEntry = haMocker.getInvocationLog().get(0);
+        assertEquals(1, (int) invocationLogEntry[0]);
+        assertEquals("Restart", invocationLogEntry[2]);
+
+        assertTrue(jopsMocker.getInvocationLog().isEmpty());
+
+        // any .jar.new files should have been deleted
+        assertFalse(targetDummyJarNew.exists());
+        assertFalse(targetBazJarNew.exists());
+
+        // dummy_plugin was fetched
+        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo(bazOldServePath)));
+
+        // the "old" baz_plugin jar has been installed
+        TestUtils.assertFileContentsEqual(this.referenceBazJarOld, this.targetBazJar);
+        // the dummy_plugin jar has not
+        assertFalse(this.targetDummyJar.exists());
+
+        // pluginmanager.version has been set to the current version
+        assertEquals(7000, Config.getPref().getInt("pluginmanager.version", 111));
+        // pluginmanager.lastupdate hasn't been updated
+        // questionably correct
+        assertEquals("999", Config.getPref().get("pluginmanager.lastupdate", "111"));
+    }
 }
Index: /trunk/test/unit/org/openstreetmap/josm/plugins/PluginHandlerJOSMTooOldTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/plugins/PluginHandlerJOSMTooOldTest.java	(revision 14212)
+++ /trunk/test/unit/org/openstreetmap/josm/plugins/PluginHandlerJOSMTooOldTest.java	(revision 14213)
@@ -67,4 +67,6 @@
         this.referenceBazJarOld = new File(TestUtils.getTestDataRoot(), "__files/plugin/baz_plugin.v6.jar");
         this.referenceBazJarNew = new File(TestUtils.getTestDataRoot(), "__files/plugin/baz_plugin.v7.jar");
+        this.referenceQuxJarOld = new File(TestUtils.getTestDataRoot(), "__files/" + referencePathQuxJarOld);
+        this.referenceQuxJarNewer = new File(TestUtils.getTestDataRoot(), "__files/" + referencePathQuxJarNewer);
         this.pluginDir = Preferences.main().getPluginsDirectory();
         this.targetDummyJar = new File(this.pluginDir, "dummy_plugin.jar");
@@ -72,6 +74,11 @@
         this.targetBazJar = new File(this.pluginDir, "baz_plugin.jar");
         this.targetBazJarNew = new File(this.pluginDir, "baz_plugin.jar.new");
+        this.targetQuxJar = new File(this.pluginDir, "qux_plugin.jar");
+        this.targetQuxJarNew = new File(this.pluginDir, "qux_plugin.jar.new");
         this.pluginDir.mkdirs();
     }
+
+    private static final String referencePathQuxJarOld = "plugin/qux_plugin.v345.jar";
+    private static final String referencePathQuxJarNewer = "plugin/qux_plugin.v432.jar";
 
     private File pluginDir;
@@ -80,8 +87,12 @@
     private File referenceBazJarOld;
     private File referenceBazJarNew;
+    private File referenceQuxJarOld;
+    private File referenceQuxJarNewer;
     private File targetDummyJar;
     private File targetDummyJarNew;
     private File targetBazJar;
     private File targetBazJarNew;
+    private File targetQuxJar;
+    private File targetQuxJarNew;
 
     private final String bazPluginVersionReqString = "JOSM version 8,001 required for plugin baz_plugin.";
@@ -283,3 +294,64 @@
         assertNotEquals(Config.getPref().get("pluginmanager.lastupdate", "999"), "999");
     }
+
+    /**
+     * When a plugin advertises several versions for compatibility with older JOSMs, but even the
+     * oldest of those is newer than our JOSM version, the user is prompted to upgrade to the newest
+     * version anyway.
+     *
+     * While this behaviour is not incorrect, it's probably less helpful than it could be - the
+     * version that's most likely to work best in this case will be the "oldest" still-available
+     * version, however this test documents the behaviour.
+     * @throws IOException never
+     */
+    @Test
+    @JOSMTestRules.OverrideAssumeRevision("Revision: 7200\n")
+    public void testUpdatePluginsMultiVersionInsufficient() throws IOException {
+        TestUtils.assumeWorkingJMockit();
+
+        final PluginServer pluginServer = new PluginServer(
+            new PluginServer.RemotePlugin(this.referenceBazJarOld),
+            new PluginServer.RemotePlugin(this.referenceQuxJarNewer, ImmutableMap.of(
+                "7499_Plugin-Url", "346;http://localhost:" + this.pluginServerRule.port() + "/dont/bother.jar"
+            ))
+        );
+        pluginServer.applyToWireMockServer(this.pluginServerRule);
+        Config.getPref().putList("plugins", ImmutableList.of("qux_plugin", "baz_plugin"));
+
+        new ExtendedDialogMocker(ImmutableMap.of("JOSM version 7,500 required for plugin qux_plugin.", "Download Plugin"));
+
+        Files.copy(this.referenceQuxJarOld.toPath(), this.targetQuxJar.toPath());
+        Files.copy(this.referenceBazJarOld.toPath(), this.targetBazJar.toPath());
+
+        final List<PluginInformation> updatedPlugins = PluginHandler.updatePlugins(
+            MainApplication.getMainFrame(),
+            null,
+            null,
+            false
+        ).stream().sorted((a, b) -> a.name.compareTo(b.name)).collect(ImmutableList.toImmutableList());
+
+        assertEquals(2, updatedPlugins.size());
+
+        assertEquals("baz_plugin", updatedPlugins.get(0).name);
+        assertEquals("6", updatedPlugins.get(0).localversion);
+
+        assertEquals("qux_plugin", updatedPlugins.get(1).name);
+        // questionably correct
+        assertEquals("432", updatedPlugins.get(1).localversion);
+
+        assertFalse(targetQuxJarNew.exists());
+
+        TestUtils.assertFileContentsEqual(this.referenceBazJarOld, this.targetBazJar);
+        // questionably correct
+        TestUtils.assertFileContentsEqual(this.referenceQuxJarNewer, this.targetQuxJar);
+
+        assertEquals(2, WireMock.getAllServeEvents().size());
+        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
+        // questionably correct
+        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/qux_plugin.v432.jar")));
+
+        assertEquals(7200, Config.getPref().getInt("pluginmanager.version", 111));
+        // not mocking the time so just check it's not its original value
+        assertNotEquals("999", Config.getPref().get("pluginmanager.lastupdate", "999"));
+    }
 }
Index: /trunk/test/unit/org/openstreetmap/josm/plugins/PluginHandlerMultiVersionTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/plugins/PluginHandlerMultiVersionTest.java	(revision 14213)
+++ /trunk/test/unit/org/openstreetmap/josm/plugins/PluginHandlerMultiVersionTest.java	(revision 14213)
@@ -0,0 +1,210 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins;
+
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.openstreetmap.josm.TestUtils;
+import org.openstreetmap.josm.data.Preferences;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+import org.openstreetmap.josm.testutils.PluginServer;
+import org.openstreetmap.josm.testutils.mockers.ExtendedDialogMocker;
+
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * Test parts of {@link PluginHandler} class with plugins that advertise multiple versions for compatibility.
+ */
+public class PluginHandlerMultiVersionTest {
+    /**
+     * Setup test.
+     */
+    @Rule
+    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+    public JOSMTestRules test = new JOSMTestRules().preferences().main();
+
+    /**
+     * Plugin server mock.
+     */
+    @Rule
+    public WireMockRule pluginServerRule = new WireMockRule(
+        options().dynamicPort().usingFilesUnderDirectory(TestUtils.getTestDataRoot())
+    );
+
+    /**
+     * Setup test.
+     */
+    @Before
+    public void setUp() {
+        Config.getPref().putInt("pluginmanager.version", 999);
+        Config.getPref().put("pluginmanager.lastupdate", "999");
+        Config.getPref().putList("pluginmanager.sites",
+            ImmutableList.of(String.format("http://localhost:%s/plugins", this.pluginServerRule.port()))
+        );
+
+        this.referenceBazJarOld = new File(TestUtils.getTestDataRoot(), "__files/plugin/baz_plugin.v6.jar");
+        this.referenceQuxJarOld = new File(TestUtils.getTestDataRoot(), "__files/" + referencePathQuxJarOld);
+        this.referenceQuxJarNewer = new File(TestUtils.getTestDataRoot(), "__files/" + referencePathQuxJarNewer);
+        this.referenceQuxJarNewest = new File(TestUtils.getTestDataRoot(), "__files/" + referencePathQuxJarNewest);
+        this.pluginDir = Preferences.main().getPluginsDirectory();
+        this.targetBazJar = new File(this.pluginDir, "baz_plugin.jar");
+        this.targetBazJarNew = new File(this.pluginDir, "baz_plugin.jar.new");
+        this.targetQuxJar = new File(this.pluginDir, "qux_plugin.jar");
+        this.targetQuxJarNew = new File(this.pluginDir, "qux_plugin.jar.new");
+        this.pluginDir.mkdirs();
+    }
+
+    private static final String referencePathQuxJarOld = "plugin/qux_plugin.v345.jar";
+    private static final String referencePathQuxJarNewer = "plugin/qux_plugin.v432.jar";
+    private static final String referencePathQuxJarNewest = "plugin/qux_plugin.v435.jar";
+
+    private File pluginDir;
+    private File referenceBazJarOld;
+    private File referenceQuxJarOld;
+    private File referenceQuxJarNewer;
+    private File referenceQuxJarNewest;
+    private File targetBazJar;
+    private File targetBazJarNew;
+    private File targetQuxJar;
+    private File targetQuxJarNew;
+
+    /**
+     * test update of plugins when our current JOSM version prevents us from using the latest version,
+     * but an additional version is listed which *does* support our version
+     * @throws Exception on failure
+     */
+    @JOSMTestRules.OverrideAssumeRevision("Revision: 7501\n")
+    @Test
+    public void testUpdatePluginsOneMultiVersion() throws Exception {
+        TestUtils.assumeWorkingJMockit();
+
+        final String quxNewerServePath = "/qux/newer.jar";
+        final PluginServer pluginServer = new PluginServer(
+            new PluginServer.RemotePlugin(this.referenceBazJarOld),
+            new PluginServer.RemotePlugin(this.referenceQuxJarNewest, ImmutableMap.of(
+                "7500_Plugin-Url", "432;http://localhost:" + this.pluginServerRule.port() + quxNewerServePath,
+                "7499_Plugin-Url", "346;http://localhost:" + this.pluginServerRule.port() + "/not/served.jar",
+                "6999_Plugin-Url", "345;http://localhost:" + this.pluginServerRule.port() + "/not/served/either.jar"
+            ))
+        );
+        pluginServer.applyToWireMockServer(this.pluginServerRule);
+        // need to actually serve this older jar from somewhere
+        this.pluginServerRule.stubFor(
+            WireMock.get(WireMock.urlEqualTo(quxNewerServePath)).willReturn(
+                WireMock.aResponse().withStatus(200).withHeader("Content-Type", "application/java-archive").withBodyFile(
+                    referencePathQuxJarNewer
+                )
+            )
+        );
+        Config.getPref().putList("plugins", ImmutableList.of("qux_plugin", "baz_plugin"));
+
+        // catch any (unexpected) attempts to show us an ExtendedDialog
+        new ExtendedDialogMocker();
+
+        Files.copy(this.referenceQuxJarOld.toPath(), this.targetQuxJar.toPath());
+        Files.copy(this.referenceBazJarOld.toPath(), this.targetBazJar.toPath());
+
+        final List<PluginInformation> updatedPlugins = PluginHandler.updatePlugins(
+            MainApplication.getMainFrame(),
+            null,
+            null,
+            false
+        ).stream().sorted((a, b) -> a.name.compareTo(b.name)).collect(ImmutableList.toImmutableList());
+
+        assertEquals(2, updatedPlugins.size());
+
+        assertEquals("baz_plugin", updatedPlugins.get(0).name);
+        assertEquals("6", updatedPlugins.get(0).localversion);
+
+        assertEquals("qux_plugin", updatedPlugins.get(1).name);
+        assertEquals("432", updatedPlugins.get(1).localversion);
+
+        assertFalse(targetBazJarNew.exists());
+        assertFalse(targetQuxJarNew.exists());
+
+        TestUtils.assertFileContentsEqual(this.referenceBazJarOld, this.targetBazJar);
+        TestUtils.assertFileContentsEqual(this.referenceQuxJarNewer, this.targetQuxJar);
+
+        assertEquals(2, WireMock.getAllServeEvents().size());
+        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
+        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo(quxNewerServePath)));
+
+        assertEquals(7501, Config.getPref().getInt("pluginmanager.version", 111));
+        // not mocking the time so just check it's not its original value
+        assertNotEquals("999", Config.getPref().get("pluginmanager.lastupdate", "999"));
+    }
+
+    /**
+     * test update of plugins when our current JOSM version prevents us from using all but the version
+     * we already have, which is still listed.
+     * @throws Exception on failure
+     */
+    @JOSMTestRules.OverrideAssumeRevision("Revision: 7000\n")
+    @Test
+    public void testUpdatePluginsExistingVersionLatestPossible() throws Exception {
+        TestUtils.assumeWorkingJMockit();
+
+        final PluginServer pluginServer = new PluginServer(
+            new PluginServer.RemotePlugin(this.referenceBazJarOld),
+            new PluginServer.RemotePlugin(this.referenceQuxJarNewest, ImmutableMap.of(
+                "7500_Plugin-Url", "432;http://localhost:" + this.pluginServerRule.port() + "/dont.jar",
+                "7499_Plugin-Url", "346;http://localhost:" + this.pluginServerRule.port() + "/even.jar",
+                "6999_Plugin-Url", "345;http://localhost:" + this.pluginServerRule.port() + "/bother.jar"
+            ))
+        );
+        pluginServer.applyToWireMockServer(this.pluginServerRule);
+        Config.getPref().putList("plugins", ImmutableList.of("qux_plugin", "baz_plugin"));
+
+        // catch any (unexpected) attempts to show us an ExtendedDialog
+        new ExtendedDialogMocker();
+
+        Files.copy(this.referenceQuxJarOld.toPath(), this.targetQuxJar.toPath());
+        Files.copy(this.referenceBazJarOld.toPath(), this.targetBazJar.toPath());
+
+        final List<PluginInformation> updatedPlugins = PluginHandler.updatePlugins(
+            MainApplication.getMainFrame(),
+            null,
+            null,
+            false
+        ).stream().sorted((a, b) -> a.name.compareTo(b.name)).collect(ImmutableList.toImmutableList());
+
+        assertEquals(2, updatedPlugins.size());
+
+        assertEquals("baz_plugin", updatedPlugins.get(0).name);
+        assertEquals("6", updatedPlugins.get(0).localversion);
+
+        assertEquals("qux_plugin", updatedPlugins.get(1).name);
+        assertEquals("345", updatedPlugins.get(1).localversion);
+
+        assertFalse(targetBazJarNew.exists());
+        assertFalse(targetQuxJarNew.exists());
+
+        // should be as before
+        TestUtils.assertFileContentsEqual(this.referenceBazJarOld, this.targetBazJar);
+        TestUtils.assertFileContentsEqual(this.referenceQuxJarOld, this.targetQuxJar);
+
+        // only the plugins list should have been downloaded
+        assertEquals(1, WireMock.getAllServeEvents().size());
+        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
+
+        assertEquals(7000, Config.getPref().getInt("pluginmanager.version", 111));
+        // not mocking the time so just check it's not its original value
+        assertNotEquals("999", Config.getPref().get("pluginmanager.lastupdate", "999"));
+    }
+}
