// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

// +build !integration

package checkpoint

import (
	"io/ioutil"
	"os"
	"path/filepath"
	"runtime"
	"testing"
	"time"

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

func eventually(t *testing.T, predicate func() (bool, error), timeout time.Duration) {
	const minInterval = time.Millisecond * 5
	const maxInterval = time.Millisecond * 500

	checkInterval := timeout / 100
	if checkInterval < minInterval {
		checkInterval = minInterval
	}
	if checkInterval > maxInterval {
		checkInterval = maxInterval
	}
	for deadline, first := time.Now().Add(timeout), true; first || time.Now().Before(deadline); first = false {
		ok, err := predicate()
		if err != nil {
			t.Fatal("predicate failed with error:", err)
			return
		}
		if ok {
			return
		}
		time.Sleep(checkInterval)
	}
	t.Fatal("predicate is not true after", timeout)
}

// Test that a write is triggered when the maximum number of updates is reached.
func TestWriteMaxUpdates(t *testing.T) {
	dir, err := ioutil.TempDir("", "wlb-checkpoint-test")
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		err := os.RemoveAll(dir)
		if err != nil {
			t.Fatal(err)
		}
	}()

	file := filepath.Join(dir, "some", "new", "dir", ".winlogbeat.yml")
	if !assert.False(t, fileExists(file), "%s should not exist", file) {
		return
	}

	cp, err := NewCheckpoint(file, 2, time.Hour)
	if err != nil {
		t.Fatal(err)
	}
	defer cp.Shutdown()

	// Send update - it's not written to disk but it's in memory.
	cp.Persist("App", 1, time.Now(), "")
	found := false
	eventually(t, func() (bool, error) {
		_, found = cp.States()["App"]
		return found, nil
	}, time.Second*15)
	assert.True(t, found)

	ps, err := cp.read()
	if err != nil {
		t.Fatal("read failed", err)
	}
	assert.Len(t, ps.States, 0)

	// Send update - it is written to disk.
	cp.Persist("App", 2, time.Now(), "")
	eventually(t, func() (bool, error) {
		ps, err = cp.read()
		return ps != nil && len(ps.States) > 0, err
	}, time.Second*15)

	if assert.Len(t, ps.States, 1, "state not written, could be a flush timing issue, retry") {
		assert.Equal(t, "App", ps.States[0].Name)
		assert.Equal(t, uint64(2), ps.States[0].RecordNumber)
	}
}

// Test that a write is triggered when the maximum time period since the last
// write is reached.
func TestWriteTimedFlush(t *testing.T) {
	dir, err := ioutil.TempDir("", "wlb-checkpoint-test")
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		err := os.RemoveAll(dir)
		if err != nil {
			t.Fatal(err)
		}
	}()

	file := filepath.Join(dir, ".winlogbeat.yml")
	if !assert.False(t, fileExists(file), "%s should not exist", file) {
		return
	}

	cp, err := NewCheckpoint(file, 100, time.Second)
	if err != nil {
		t.Fatal(err)
	}
	defer cp.Shutdown()

	// Send update then wait longer than the flush interval and it should be
	// on disk.
	cp.Persist("App", 1, time.Now(), "")
	eventually(t, func() (bool, error) {
		ps, err := cp.read()
		return ps != nil && len(ps.States) > 0, err
	}, time.Second*15)

	ps, err := cp.read()
	if err != nil {
		t.Fatal("read failed", err)
	}
	if assert.Len(t, ps.States, 1) {
		assert.Equal(t, "App", ps.States[0].Name)
		assert.Equal(t, uint64(1), ps.States[0].RecordNumber)
	}
}

// Test that createDir creates the directory with 0750 permissions.
func TestCreateDir(t *testing.T) {
	dir, err := ioutil.TempDir("", "wlb-checkpoint-test")
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		err := os.RemoveAll(dir)
		if err != nil {
			t.Fatal(err)
		}
	}()

	stateDir := filepath.Join(dir, "state", "dir", "does", "not", "exists")
	file := filepath.Join(stateDir, ".winlogbeat.yml")
	cp := &Checkpoint{file: file}

	if !assert.False(t, fileExists(file), "%s should not exist", file) {
		return
	}
	if err = cp.createDir(); err != nil {
		t.Fatal("createDir", err)
	}
	if !assert.True(t, fileExists(stateDir), "%s should exist", file) {
		return
	}

	// mkdir on Windows does not pass the POSIX mode to the CreateDirectory
	// syscall so doesn't test the mode.
	if runtime.GOOS != "windows" {
		fileInfo, err := os.Stat(stateDir)
		if assert.NoError(t, err) {
			assert.Equal(t, true, fileInfo.IsDir())
			assert.Equal(t, os.FileMode(0750), fileInfo.Mode().Perm())
		}
	}
}

// Test createDir when the directory already exists to verify that no error is
// returned.
func TestCreateDirAlreadyExists(t *testing.T) {
	dir, err := ioutil.TempDir("", "wlb-checkpoint-test")
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		err := os.RemoveAll(dir)
		if err != nil {
			t.Fatal(err)
		}
	}()

	file := filepath.Join(dir, ".winlogbeat.yml")
	cp := &Checkpoint{file: file}

	if !assert.True(t, fileExists(dir), "%s should exist", file) {
		return
	}
	assert.NoError(t, cp.createDir())
}

// fileExists returns true if the specified file exists.
func fileExists(file string) bool {
	_, err := os.Stat(file)
	return !os.IsNotExist(err)
}
