//
//  ========================================================================
//  Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.util;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.condition.OS.WINDOWS;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.Scanner.Notification;
import org.junit.jupiter.api.AfterAll;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
import org.junit.jupiter.api.condition.DisabledOnOs;

public class ScannerTest
{
    static File _directory;
    static Scanner _scanner;
    static BlockingQueue<Event> _queue = new LinkedBlockingQueue<Event>();
    static BlockingQueue<List<String>> _bulk = new LinkedBlockingQueue<List<String>>();

    @BeforeAll
    public static void setUpBeforeClass() throws Exception
    {
        File testDir = MavenTestingUtils.getTargetTestingDir(ScannerTest.class.getSimpleName());
        FS.ensureEmpty(testDir);
        
        // Use full path, pointing to a real directory (for FileSystems that are case-insensitive, like Windows and OSX to use)
        // This is only needed for the various comparisons below to make sense.
        _directory = testDir.toPath().toRealPath().toFile();

        _scanner = new Scanner();
        _scanner.addScanDir(_directory);
        _scanner.setScanInterval(0);
        _scanner.addListener(new Scanner.DiscreteListener()
        {
            @Override
            public void fileRemoved(String filename) throws Exception
            {
                _queue.add(new Event(filename,Notification.REMOVED));
            }

            @Override
            public void fileChanged(String filename) throws Exception
            {
                _queue.add(new Event(filename,Notification.CHANGED));
            }

            @Override
            public void fileAdded(String filename) throws Exception
            {
                _queue.add(new Event(filename,Notification.ADDED));
            }
        });
        _scanner.addListener(new Scanner.BulkListener()
        {
            @Override
            public void filesChanged(List<String> filenames) throws Exception
            {
                _bulk.add(filenames);
            }
        });
        _scanner.start();

        _scanner.scan();
        
        assertTrue(_queue.isEmpty());
        assertTrue(_bulk.isEmpty());
    }

    @AfterAll
    public static void tearDownAfterClass() throws Exception
    {
        _scanner.stop();
        IO.delete(_directory);
    }

    static class Event
    {
        String _filename;
        Scanner.Notification _notification;

        public Event(String filename, Notification notification)
        {
            _filename=filename;
            _notification=notification;
        }
    }

    @Test
    @DisabledOnOs(WINDOWS) // TODO: needs review
    @DisabledIfSystemProperty(named = "env", matches = "ci") // TODO: SLOW, needs review
    public void testAddedChangeRemove() throws Exception
    {
        touch("a0");

        // takes 2 scans to notice a0 and check that it is stable
        _scanner.scan();
        _scanner.scan();
        Event event = _queue.poll();
        assertNotNull(event, "Event should not be null");
        assertEquals(_directory+"/a0",event._filename);
        assertEquals(Notification.ADDED,event._notification);

        // add 3 more files
        Thread.sleep(1100); // make sure time in seconds changes
        touch("a1");
        touch("a2");
        touch("a3");

        // not stable after 1 scan so should not be seen yet.
        _scanner.scan();
        event = _queue.poll();
        assertTrue(event==null);

        // Keep a2 unstable and remove a3 before it stabalized
        Thread.sleep(1100); // make sure time in seconds changes
        touch("a2");
        delete("a3");

        // only a1 is stable so it should be seen.
        _scanner.scan();
        event = _queue.poll();
        assertTrue(event!=null);
        assertEquals(_directory+"/a1",event._filename);
        assertEquals(Notification.ADDED,event._notification);
        assertTrue(_queue.isEmpty());

        // Now a2 is stable
        _scanner.scan();
        event = _queue.poll();
        assertTrue(event!=null);
        assertEquals(_directory+"/a2",event._filename);
        assertEquals(Notification.ADDED,event._notification);
        assertTrue(_queue.isEmpty());

        // We never see a3 as it was deleted before it stabalised

        // touch a1 and a2
        Thread.sleep(1100); // make sure time in seconds changes
        touch("a1");
        touch("a2");
        // not stable after 1scan so nothing should not be seen yet.
        _scanner.scan();
        event = _queue.poll();
        assertTrue(event==null);

        // Keep a2 unstable
        Thread.sleep(1100); // make sure time in seconds changes
        touch("a2");

        // only a1 is stable so it should be seen.
        _scanner.scan();
        event = _queue.poll();
        assertTrue(event!=null);
        assertEquals(_directory+"/a1",event._filename);
        assertEquals(Notification.CHANGED,event._notification);
        assertTrue(_queue.isEmpty());

        // Now a2 is stable
        _scanner.scan();
        event = _queue.poll();
        assertTrue(event!=null);
        assertEquals(_directory+"/a2",event._filename);
        assertEquals(Notification.CHANGED,event._notification);
        assertTrue(_queue.isEmpty());


        // delete a1 and a2
        delete("a1");
        delete("a2");
        // not stable after 1scan so nothing should not be seen yet.
        _scanner.scan();
        event = _queue.poll();
        assertTrue(event==null);

        // readd a2
        touch("a2");

        // only a1 is stable so it should be seen.
        _scanner.scan();
        event = _queue.poll();
        assertTrue(event!=null);
        assertEquals(_directory+"/a1",event._filename);
        assertEquals(Notification.REMOVED,event._notification);
        assertTrue(_queue.isEmpty());

        // Now a2 is stable and is a changed file rather than a remove
        _scanner.scan();
        event = _queue.poll();
        assertTrue(event!=null);
        assertEquals(_directory+"/a2",event._filename);
        assertEquals(Notification.CHANGED,event._notification);
        assertTrue(_queue.isEmpty());

    }

    @Test
    @DisabledOnOs(WINDOWS) // TODO: needs review
    public void testSizeChange() throws Exception
    {
        touch("tsc0");
        _scanner.scan();
        _scanner.scan();

        // takes 2s to notice tsc0 and check that it is stable.  This syncs us with the scan
        Event event = _queue.poll();
        assertTrue(event!=null);
        assertEquals(_directory+"/tsc0",event._filename);
        assertEquals(Notification.ADDED,event._notification);


        // Create a new file by writing to it.
        long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
        File file = new File(_directory,"st");
        try (OutputStream out = new FileOutputStream(file,true))
        {
            out.write('x');
            out.flush();
            file.setLastModified(now);

            // Not stable yet so no notification.
            _scanner.scan();
            event = _queue.poll();
            assertTrue(event==null);

            // Modify size only
            out.write('x');
            out.flush();
            file.setLastModified(now);

            // Still not stable yet so no notification.
            _scanner.scan();
            event = _queue.poll();
            assertTrue(event==null);

            // now stable so finally see the ADDED
            _scanner.scan();
            event = _queue.poll();
            assertTrue(event!=null);
            assertEquals(_directory+"/st",event._filename);
            assertEquals(Notification.ADDED,event._notification);

            // Modify size only
            out.write('x');
            out.flush();
            file.setLastModified(now);


            // Still not stable yet so no notification.
            _scanner.scan();
            event = _queue.poll();
            assertTrue(event==null);

            // now stable so finally see the ADDED
            _scanner.scan();
            event = _queue.poll();
            assertTrue(event!=null);
            assertEquals(_directory+"/st",event._filename);
            assertEquals(Notification.CHANGED,event._notification);
        }
    }

    private void delete(String string) throws IOException
    {
        File file = new File(_directory,string);
        if (file.exists())
            IO.delete(file);
    }

    private void touch(String string) throws IOException
    {
        File file = new File(_directory,string);
        if (file.exists())
            file.setLastModified(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()));
        else
            file.createNewFile();
    }
}
