// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build linux

package fsnotify

import (
	"os"
	"path/filepath"
	"syscall"
	"testing"
	"time"
)

func TestInotifyCloseRightAway(t *testing.T) {
	w, err := NewWatcher()
	if err != nil {
		t.Fatalf("Failed to create watcher")
	}

	// Close immediately; it won't even reach the first syscall.Read.
	w.Close()

	// Wait for the close to complete.
	<-time.After(50 * time.Millisecond)
	isWatcherReallyClosed(t, w)
}

func TestInotifyCloseSlightlyLater(t *testing.T) {
	w, err := NewWatcher()
	if err != nil {
		t.Fatalf("Failed to create watcher")
	}

	// Wait until readEvents has reached syscall.Read, and Close.
	<-time.After(50 * time.Millisecond)
	w.Close()

	// Wait for the close to complete.
	<-time.After(50 * time.Millisecond)
	isWatcherReallyClosed(t, w)
}

func TestInotifyCloseSlightlyLaterWithWatch(t *testing.T) {
	testDir := tempMkdir(t)
	defer os.RemoveAll(testDir)

	w, err := NewWatcher()
	if err != nil {
		t.Fatalf("Failed to create watcher")
	}
	w.Add(testDir)

	// Wait until readEvents has reached syscall.Read, and Close.
	<-time.After(50 * time.Millisecond)
	w.Close()

	// Wait for the close to complete.
	<-time.After(50 * time.Millisecond)
	isWatcherReallyClosed(t, w)
}

func TestInotifyCloseAfterRead(t *testing.T) {
	testDir := tempMkdir(t)
	defer os.RemoveAll(testDir)

	w, err := NewWatcher()
	if err != nil {
		t.Fatalf("Failed to create watcher")
	}

	err = w.Add(testDir)
	if err != nil {
		t.Fatalf("Failed to add .")
	}

	// Generate an event.
	os.Create(filepath.Join(testDir, "somethingSOMETHINGsomethingSOMETHING"))

	// Wait for readEvents to read the event, then close the watcher.
	<-time.After(50 * time.Millisecond)
	w.Close()

	// Wait for the close to complete.
	<-time.After(50 * time.Millisecond)
	isWatcherReallyClosed(t, w)
}

func isWatcherReallyClosed(t *testing.T, w *Watcher) {
	select {
	case err, ok := <-w.Errors:
		if ok {
			t.Fatalf("w.Errors is not closed; readEvents is still alive after closing (error: %v)", err)
		}
	default:
		t.Fatalf("w.Errors would have blocked; readEvents is still alive!")
	}

	select {
	case _, ok := <-w.Events:
		if ok {
			t.Fatalf("w.Events is not closed; readEvents is still alive after closing")
		}
	default:
		t.Fatalf("w.Events would have blocked; readEvents is still alive!")
	}
}

func TestInotifyCloseCreate(t *testing.T) {
	testDir := tempMkdir(t)
	defer os.RemoveAll(testDir)

	w, err := NewWatcher()
	if err != nil {
		t.Fatalf("Failed to create watcher: %v", err)
	}
	defer w.Close()

	err = w.Add(testDir)
	if err != nil {
		t.Fatalf("Failed to add testDir: %v", err)
	}
	h, err := os.Create(filepath.Join(testDir, "testfile"))
	if err != nil {
		t.Fatalf("Failed to create file in testdir: %v", err)
	}
	h.Close()
	select {
	case _ = <-w.Events:
	case err := <-w.Errors:
		t.Fatalf("Error from watcher: %v", err)
	case <-time.After(50 * time.Millisecond):
		t.Fatalf("Took too long to wait for event")
	}

	// At this point, we've received one event, so the goroutine is ready.
	// It's also blocking on syscall.Read.
	// Now we try to swap the file descriptor under its nose.
	w.Close()
	w, err = NewWatcher()
	defer w.Close()
	if err != nil {
		t.Fatalf("Failed to create second watcher: %v", err)
	}

	<-time.After(50 * time.Millisecond)
	err = w.Add(testDir)
	if err != nil {
		t.Fatalf("Error adding testDir again: %v", err)
	}
}

func TestInotifyStress(t *testing.T) {
	testDir := tempMkdir(t)
	defer os.RemoveAll(testDir)
	testFile := filepath.Join(testDir, "testfile")

	w, err := NewWatcher()
	if err != nil {
		t.Fatalf("Failed to create watcher: %v", err)
	}
	defer w.Close()

	killchan := make(chan struct{})
	defer close(killchan)

	err = w.Add(testDir)
	if err != nil {
		t.Fatalf("Failed to add testDir: %v", err)
	}

	proc, err := os.FindProcess(os.Getpid())
	if err != nil {
		t.Fatalf("Error finding process: %v", err)
	}

	go func() {
		for {
			select {
			case <-time.After(5 * time.Millisecond):
				err := proc.Signal(syscall.SIGUSR1)
				if err != nil {
					t.Fatalf("Signal failed: %v", err)
				}
			case <-killchan:
				return
			}
		}
	}()

	go func() {
		for {
			select {
			case <-time.After(11 * time.Millisecond):
				err := w.poller.wake()
				if err != nil {
					t.Fatalf("Wake failed: %v", err)
				}
			case <-killchan:
				return
			}
		}
	}()

	go func() {
		for {
			select {
			case <-killchan:
				return
			default:
				handle, err := os.Create(testFile)
				if err != nil {
					t.Fatalf("Create failed: %v", err)
				}
				handle.Close()
				time.Sleep(time.Millisecond)
				err = os.Remove(testFile)
				if err != nil {
					t.Fatalf("Remove failed: %v", err)
				}
			}
		}
	}()

	creates := 0
	removes := 0
	after := time.After(5 * time.Second)
	for {
		select {
		case <-after:
			if creates-removes > 1 || creates-removes < -1 {
				t.Fatalf("Creates and removes should not be off by more than one: %d creates, %d removes", creates, removes)
			}
			if creates < 50 {
				t.Fatalf("Expected at least 50 creates, got %d", creates)
			}
			return
		case err := <-w.Errors:
			t.Fatalf("Got an error from watcher: %v", err)
		case evt := <-w.Events:
			if evt.Name != testFile {
				t.Fatalf("Got an event for an unknown file: %s", evt.Name)
			}
			if evt.Op == Create {
				creates++
			}
			if evt.Op == Remove {
				removes++
			}
		}
	}
}

func TestInotifyRemoveTwice(t *testing.T) {
	testDir := tempMkdir(t)
	defer os.RemoveAll(testDir)
	testFile := filepath.Join(testDir, "testfile")

	handle, err := os.Create(testFile)
	if err != nil {
		t.Fatalf("Create failed: %v", err)
	}
	handle.Close()

	w, err := NewWatcher()
	if err != nil {
		t.Fatalf("Failed to create watcher: %v", err)
	}
	defer w.Close()

	err = w.Add(testFile)
	if err != nil {
		t.Fatalf("Failed to add testFile: %v", err)
	}

	err = os.Remove(testFile)
	if err != nil {
		t.Fatalf("Failed to remove testFile: %v", err)
	}

	err = w.Remove(testFile)
	if err != syscall.EINVAL {
		t.Fatalf("Expected EINVAL from Remove, got: %v", err)
	}

	err = w.Remove(testFile)
	if err == syscall.EINVAL {
		t.Fatalf("Got EINVAL again, watch was not removed")
	}
}
