package fileutil

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestWatcher(t *testing.T) {
	t.Parallel()

	tmpdir := t.TempDir()

	err := os.WriteFile(filepath.Join(tmpdir, "test1.txt"), []byte{1}, 0o666)
	require.NoError(t, err)

	w := NewWatcher()
	defer w.Close()
	w.Watch([]string{filepath.Join(tmpdir, "test1.txt")})

	ch := w.Bind()
	t.Cleanup(func() { w.Unbind(ch) })

	err = os.WriteFile(filepath.Join(tmpdir, "test1.txt"), []byte{1, 2}, 0o666)
	require.NoError(t, err)

	expectChange(t, ch)
}

func TestWatcherSymlink(t *testing.T) {
	t.Parallel()

	tmpdir := t.TempDir()

	err := os.WriteFile(filepath.Join(tmpdir, "test1.txt"), []byte{1}, 0o666)
	require.NoError(t, err)

	err = os.WriteFile(filepath.Join(tmpdir, "test2.txt"), []byte{1, 2}, 0o666)
	require.NoError(t, err)

	assert.NoError(t, os.Symlink(filepath.Join(tmpdir, "test1.txt"), filepath.Join(tmpdir, "symlink1.txt")))

	w := NewWatcher()
	defer w.Close()
	w.Watch([]string{filepath.Join(tmpdir, "symlink1.txt")})

	ch := w.Bind()
	t.Cleanup(func() { w.Unbind(ch) })

	assert.NoError(t, os.WriteFile(filepath.Join(tmpdir, "test1.txt"), []byte{1, 2, 3}, 0o666))

	expectChange(t, ch)

	assert.NoError(t, os.Symlink(filepath.Join(tmpdir, "test2.txt"), filepath.Join(tmpdir, "symlink2.txt")))
	assert.NoError(t, os.Rename(filepath.Join(tmpdir, "symlink2.txt"), filepath.Join(tmpdir, "symlink1.txt")))

	expectChange(t, ch)
}

func TestWatcher_FileRemoval(t *testing.T) {
	t.Parallel()

	tmpdir := t.TempDir()

	err := os.WriteFile(filepath.Join(tmpdir, "test1.txt"), []byte{1}, 0o666)
	require.NoError(t, err)

	w := NewWatcher()
	defer w.Close()
	w.Watch([]string{filepath.Join(tmpdir, "test1.txt")})

	ch := w.Bind()
	t.Cleanup(func() { w.Unbind(ch) })

	err = os.Remove(filepath.Join(tmpdir, "test1.txt"))
	require.NoError(t, err)

	expectChange(t, ch)

	err = os.WriteFile(filepath.Join(tmpdir, "test1.txt"), []byte{1, 2}, 0o666)
	require.NoError(t, err)

	expectChange(t, ch)
}

func TestWatcher_FileModification(t *testing.T) {
	t.Parallel()

	tmpdir := t.TempDir()
	nm := filepath.Join(tmpdir, "test1.txt")
	now := time.Now()

	require.NoError(t, os.WriteFile(nm, []byte{1, 2, 3, 4}, 0o666))
	require.NoError(t, os.Chtimes(nm, now, now))

	w := NewWatcher()
	defer w.Close()
	w.Watch([]string{nm})

	ch := w.Bind()
	t.Cleanup(func() { w.Unbind(ch) })

	require.NoError(t, os.WriteFile(nm, []byte{5, 6, 7, 8}, 0o666))
	require.NoError(t, os.Chtimes(nm, now, now))

	expectChange(t, ch)
}

func TestWatcher_UnWatch(t *testing.T) {
	t.Parallel()

	tmpdir := t.TempDir()
	nm := filepath.Join(tmpdir, "test1.txt")
	now := time.Now()

	require.NoError(t, os.WriteFile(nm, []byte{1, 2, 3}, 0o666))
	require.NoError(t, os.Chtimes(nm, now, now))

	w := NewWatcher()
	defer w.Close()

	ch := w.Bind()
	t.Cleanup(func() { w.Unbind(ch) })

	w.Watch([]string{nm})
	require.NoError(t, os.WriteFile(nm, []byte{4, 5, 6}, 0o666))
	require.NoError(t, os.Chtimes(nm, now, now))
	expectChange(t, ch)

	w.Watch(nil)
	require.NoError(t, os.WriteFile(nm, []byte{7, 8, 9}, 0o666))
	require.NoError(t, os.Chtimes(nm, now, now))
	expectNoChange(t, ch)
}

func expectChange(t *testing.T, ch chan context.Context) {
	t.Helper()

	cnt := 0
	select {
	case <-ch:
		cnt++
	case <-time.After(2 * pollingInterval):
	}
	assert.Greater(t, cnt, 0, "should signal a change")
}

func expectNoChange(t *testing.T, ch chan context.Context) {
	t.Helper()

	cnt := 0
	select {
	case <-ch:
		cnt++
	case <-time.After(2 * pollingInterval):
	}
	assert.Equal(t, 0, cnt, "should not signal a change")
}