// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.AutosaveTask.AutosaveLayerInfo;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.testutils.JOSMTestRules;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
 * Unit tests for class {@link AutosaveTask}.
 */
public class AutosaveTaskTest {
    /**
     * We need preferences and a home directory for this.
     */
    @Rule
    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
    public JOSMTestRules test = new JOSMTestRules().preferences().platform().projection();

    private AutosaveTask task;

    /**
     * Setup test.
     * @throws IOException if autosave directory cannot be created
     */
    @Before
    public void setUp() throws IOException {
        task = new AutosaveTask();
    }

    /**
     * Unit test to {@link AutosaveTask#getUnsavedLayersFiles} - empty case
     */
    @Test
    public void testGetUnsavedLayersFilesEmpty() {
        assertTrue(task.getUnsavedLayersFiles().isEmpty());
    }

    /**
     * Unit test to {@link AutosaveTask#getUnsavedLayersFiles} - non empty case
     * @throws IOException in case of I/O error
     */
    @Test
    public void testGetUnsavedLayersFilesNotEmpty() throws IOException {
        Files.createDirectories(task.getAutosaveDir());
        String autodir = task.getAutosaveDir().toString();
        File layer1 = Files.createFile(Paths.get(autodir, "layer1.osm")).toFile();
        File layer2 = Files.createFile(Paths.get(autodir, "layer2.osm")).toFile();
        File dir = Files.createDirectory(Paths.get(autodir, "dir.osm")).toFile();
        List<File> files = task.getUnsavedLayersFiles();
        assertEquals(2, files.size());
        assertTrue(files.contains(layer1));
        assertTrue(files.contains(layer2));
        assertFalse(files.contains(dir));
    }

    /**
     * Unit test to {@link AutosaveTask#getNewLayerFile}
     * @throws IOException in case of I/O error
     */
    @Test
    public void testGetNewLayerFile() throws IOException {
        Files.createDirectories(task.getAutosaveDir());
        AutosaveLayerInfo info = new AutosaveLayerInfo(new OsmDataLayer(new DataSet(), "layer", null));
        Calendar cal = Calendar.getInstance();
        cal.set(2016, 0, 1, 1, 2, 3);
        cal.set(Calendar.MILLISECOND, 456);
        Date fixed = cal.getTime();

        AutosaveTask.PROP_INDEX_LIMIT.put(5);
        for (int i = 0; i <= AutosaveTask.PROP_INDEX_LIMIT.get() + 2; i++) {
            // Only retry 2 indexes to avoid 1000*1000 disk operations
            File f = task.getNewLayerFile(info, fixed, Math.max(0, i - 2));
            if (i > AutosaveTask.PROP_INDEX_LIMIT.get()) {
                assertNull(f);
            } else {
                assertNotNull(f);
                File pid = task.getPidFile(f);
                assertTrue(pid.exists());
                assertTrue(f.exists());
                if (i == 0) {
                    assertEquals("null_20160101_010203456.osm", f.getName());
                    assertEquals("null_20160101_010203456.pid", pid.getName());
                } else {
                    assertEquals("null_20160101_010203456_" + i + ".osm", f.getName());
                    assertEquals("null_20160101_010203456_" + i + ".pid", pid.getName());
                }
            }
        }
    }

    /**
     * Tests if {@link AutosaveTask#schedule()} creates the directories.
     */
    @Test
    public void testScheduleCreatesDirectories() {
        try {
            task.schedule();
            assertTrue(task.getAutosaveDir().toFile().isDirectory());
        } finally {
            task.cancel();
        }
    }

    /**
     * Tests that {@link AutosaveTask#run()} saves every layer
     */
    @Test
    public void testAutosaveIgnoresUnmodifiedLayer() {
        OsmDataLayer layer = new OsmDataLayer(new DataSet(), "OsmData", null);
        Main.getLayerManager().addLayer(layer);
        try {
            task.schedule();
            assertEquals(0, countFiles());
            task.run();
            assertEquals(0, countFiles());
        } finally {
            task.cancel();
        }
    }

    private int countFiles() {
        return task.getAutosaveDir().toFile().list(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.endsWith(".osm");
            }
        }).length;
    }

    /**
     * Tests that {@link AutosaveTask#run()} saves every layer.
     */
    @Test
    public void testAutosaveSavesLayer() {
        runAutosaveTaskSeveralTimes(1);
    }

    /**
     * Tests that {@link AutosaveTask#run()} saves every layer.
     */
    @Test
    public void testAutosaveSavesLayerMultipleTimes() {
        AutosaveTask.PROP_FILES_PER_LAYER.put(3);
        runAutosaveTaskSeveralTimes(5);
    }

    private void runAutosaveTaskSeveralTimes(int times) {
        DataSet data = new DataSet();
        OsmDataLayer layer = new OsmDataLayer(data, "OsmData", null);
        Main.getLayerManager().addLayer(layer);
        try {
            task.schedule();
            assertEquals(0, countFiles());

            for (int i = 0; i < times; i++) {
                data.addPrimitive(new Node(new LatLon(10, 10)));
                task.run();
                assertEquals(Math.min(i + 1, 3), countFiles());
            }

        } finally {
            task.cancel();
        }
    }

    /**
     * Tests that {@link AutosaveTask#discardUnsavedLayers()} ignores layers from the current instance
     * @throws IOException in case of I/O error
     */
    @Test
    public void testDiscardUnsavedLayersIgnoresCurrentInstance() throws IOException {
        runAutosaveTaskSeveralTimes(1);
        try (FileWriter file = new FileWriter(new File(task.getAutosaveDir().toFile(), "any_other_file.osm"))) {
            file.append("");
        }
        assertEquals(2, countFiles());

        task.discardUnsavedLayers();
        assertEquals(1, countFiles());
    }

    /**
     * Tests that {@link AutosaveTask#run()} handles duplicate layers
     */
    @Test
    public void testAutosaveHandlesDupplicateNames() {
        DataSet data1 = new DataSet();
        OsmDataLayer layer1 = new OsmDataLayer(data1, "OsmData", null);
        Main.getLayerManager().addLayer(layer1);

        DataSet data2 = new DataSet();
        OsmDataLayer layer2 = new OsmDataLayer(data2, "OsmData", null);

        try {
            task.schedule();
            assertEquals(0, countFiles());
            // also test adding layer later
            Main.getLayerManager().addLayer(layer2);

            data1.addPrimitive(new Node(new LatLon(10, 10)));
            data2.addPrimitive(new Node(new LatLon(10, 10)));
            task.run();
            assertEquals(2, countFiles());
        } finally {
            task.cancel();
        }
    }

    /**
     * Test that {@link AutosaveTask#recoverUnsavedLayers()} recovers unsaved layers.
     * @throws Exception in case of error
     */
    @Test
    public void testRecoverLayers() throws Exception {
        runAutosaveTaskSeveralTimes(1);
        try (FileWriter file = new FileWriter(new File(task.getAutosaveDir().toFile(), "any_other_file.osm"))) {
            file.append("<?xml version=\"1.0\"?><osm version=\"0.6\"><node id=\"1\" lat=\"1\" lon=\"2\" version=\"1\"/></osm>");
        }

        assertEquals(2, countFiles());
        task.recoverUnsavedLayers().get();

        assertEquals(1, countFiles());
    }
}
