package testing

import (
	"bytes"
	"context"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	platform "github.com/influxdata/influxdb/v2"
)

var keyValueLogCmpOptions = cmp.Options{
	cmp.Comparer(func(x, y []byte) bool {
		return bytes.Equal(x, y)
	}),
}

// A log entry is a comparable data structure that is used for testing
type LogEntry struct {
	Key   []byte
	Value []byte
	Time  time.Time
}

// KeyValueLogFields will include the IDGenerator, and keyValueLogs
type KeyValueLogFields struct {
	LogEntries []LogEntry
}

// KeyValueLog tests all the service functions.
func KeyValueLog(
	init func(KeyValueLogFields, *testing.T) (platform.KeyValueLog, func()), t *testing.T,
) {
	tests := []struct {
		name string
		fn   func(init func(KeyValueLogFields, *testing.T) (platform.KeyValueLog, func()),
			t *testing.T)
	}{
		{
			name: "AddLogEntry",
			fn:   AddLogEntry,
		},
		{
			name: "ForEachLogEntry",
			fn:   ForEachLogEntry,
		},
		{
			name: "FirstLogEntry",
			fn:   FirstLogEntry,
		},
		{
			name: "LastLogEntry",
			fn:   LastLogEntry,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt := tt
			t.Parallel()
			tt.fn(init, t)
		})
	}
}

// AddLogEntry tests the AddLogEntry for the KeyValueLog contract
func AddLogEntry(
	init func(KeyValueLogFields, *testing.T) (platform.KeyValueLog, func()),
	t *testing.T,
) {
	type args struct {
		key   []byte
		value []byte
		time  time.Time
	}
	type wants struct {
		err        error
		logEntries []LogEntry
	}

	tests := []struct {
		name   string
		fields KeyValueLogFields
		args   args
		wants  wants
	}{
		{
			name:   "Add entry to empty log",
			fields: KeyValueLogFields{},
			args: args{
				key:   []byte("abc"),
				value: []byte("hello"),
				time:  time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
			},
			wants: wants{
				logEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("hello"),
						Time:  time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
					},
				},
			},
		},
		{
			name: "Add entry to non-empty log",
			fields: KeyValueLogFields{
				LogEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("hat"),
						Time:  time.Date(2009, time.November, 10, 22, 0, 0, 0, time.UTC),
					},
				},
			},
			args: args{
				key:   []byte("abc"),
				value: []byte("hello"),
				time:  time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
			},
			wants: wants{
				logEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("hat"),
						Time:  time.Date(2009, time.November, 10, 22, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("hello"),
						Time:  time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
					},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, done := init(tt.fields, t)
			defer done()
			ctx := context.Background()
			err := s.AddLogEntry(ctx, tt.args.key, tt.args.value, tt.args.time)
			if (err != nil) != (tt.wants.err != nil) {
				t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)
			}

			if err != nil && tt.wants.err != nil {
				if err.Error() != tt.wants.err.Error() {
					t.Fatalf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error())
				}
			}

			logEntries := []LogEntry{}
			opts := platform.FindOptions{}
			err = s.ForEachLogEntry(ctx, tt.args.key, opts, func(v []byte, t time.Time) error {
				logEntries = append(logEntries, LogEntry{
					Key:   tt.args.key,
					Value: v,
					Time:  t,
				})
				return nil
			})
			if err != nil {
				t.Fatalf("failed to retrieve log entries: %v", err)
			}
			if diff := cmp.Diff(logEntries, tt.wants.logEntries, keyValueLogCmpOptions...); diff != "" {
				t.Errorf("logEntries are different -got/+want\ndiff %s", diff)
			}
		})
	}
}

// ForEachLogEntry tests the AddLogEntry for the KeyValueLog contract
func ForEachLogEntry(
	init func(KeyValueLogFields, *testing.T) (platform.KeyValueLog, func()),
	t *testing.T,
) {
	type args struct {
		key  []byte
		opts platform.FindOptions
	}
	type wants struct {
		err        error
		logEntries []LogEntry
	}

	tests := []struct {
		name   string
		fields KeyValueLogFields
		args   args
		wants  wants
	}{
		{
			name: "all log entries",
			fields: KeyValueLogFields{
				LogEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("1"),
						Time:  time.Date(2009, time.November, 10, 1, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("2"),
						Time:  time.Date(2009, time.November, 10, 2, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("3"),
						Time:  time.Date(2009, time.November, 10, 3, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("4"),
						Time:  time.Date(2009, time.November, 10, 4, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("5"),
						Time:  time.Date(2009, time.November, 10, 5, 0, 0, 0, time.UTC),
					},
				},
			},
			args: args{
				key:  []byte("abc"),
				opts: platform.FindOptions{},
			},
			wants: wants{
				logEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("1"),
						Time:  time.Date(2009, time.November, 10, 1, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("2"),
						Time:  time.Date(2009, time.November, 10, 2, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("3"),
						Time:  time.Date(2009, time.November, 10, 3, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("4"),
						Time:  time.Date(2009, time.November, 10, 4, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("5"),
						Time:  time.Date(2009, time.November, 10, 5, 0, 0, 0, time.UTC),
					},
				},
			},
		},
		{
			name: "all log entries descending order",
			fields: KeyValueLogFields{
				LogEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("1"),
						Time:  time.Date(2009, time.November, 10, 1, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("2"),
						Time:  time.Date(2009, time.November, 10, 2, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("3"),
						Time:  time.Date(2009, time.November, 10, 3, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("4"),
						Time:  time.Date(2009, time.November, 10, 4, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("5"),
						Time:  time.Date(2009, time.November, 10, 5, 0, 0, 0, time.UTC),
					},
				},
			},
			args: args{
				key: []byte("abc"),
				opts: platform.FindOptions{
					Descending: true,
				},
			},
			wants: wants{
				logEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("5"),
						Time:  time.Date(2009, time.November, 10, 5, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("4"),
						Time:  time.Date(2009, time.November, 10, 4, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("3"),
						Time:  time.Date(2009, time.November, 10, 3, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("2"),
						Time:  time.Date(2009, time.November, 10, 2, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("1"),
						Time:  time.Date(2009, time.November, 10, 1, 0, 0, 0, time.UTC),
					},
				},
			},
		},
		{
			name: "all log entries with offset",
			fields: KeyValueLogFields{
				LogEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("1"),
						Time:  time.Date(2009, time.November, 10, 1, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("2"),
						Time:  time.Date(2009, time.November, 10, 2, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("3"),
						Time:  time.Date(2009, time.November, 10, 3, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("4"),
						Time:  time.Date(2009, time.November, 10, 4, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("5"),
						Time:  time.Date(2009, time.November, 10, 5, 0, 0, 0, time.UTC),
					},
				},
			},
			args: args{
				key: []byte("abc"),
				opts: platform.FindOptions{
					Offset: 2,
				},
			},
			wants: wants{
				logEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("3"),
						Time:  time.Date(2009, time.November, 10, 3, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("4"),
						Time:  time.Date(2009, time.November, 10, 4, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("5"),
						Time:  time.Date(2009, time.November, 10, 5, 0, 0, 0, time.UTC),
					},
				},
			},
		},
		{
			name: "for each log entry with limit",
			fields: KeyValueLogFields{
				LogEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("1"),
						Time:  time.Date(2009, time.November, 10, 1, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("2"),
						Time:  time.Date(2009, time.November, 10, 2, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("3"),
						Time:  time.Date(2009, time.November, 10, 3, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("4"),
						Time:  time.Date(2009, time.November, 10, 4, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("5"),
						Time:  time.Date(2009, time.November, 10, 5, 0, 0, 0, time.UTC),
					},
				},
			},
			args: args{
				key: []byte("abc"),
				opts: platform.FindOptions{
					Limit: 3,
				},
			},
			wants: wants{
				logEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("1"),
						Time:  time.Date(2009, time.November, 10, 1, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("2"),
						Time:  time.Date(2009, time.November, 10, 2, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("3"),
						Time:  time.Date(2009, time.November, 10, 3, 0, 0, 0, time.UTC),
					},
				},
			},
		},
		{
			name: "log entries with offset and limit",
			fields: KeyValueLogFields{
				LogEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("1"),
						Time:  time.Date(2009, time.November, 10, 1, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("2"),
						Time:  time.Date(2009, time.November, 10, 2, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("3"),
						Time:  time.Date(2009, time.November, 10, 3, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("4"),
						Time:  time.Date(2009, time.November, 10, 4, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("5"),
						Time:  time.Date(2009, time.November, 10, 5, 0, 0, 0, time.UTC),
					},
				},
			},
			args: args{
				key: []byte("abc"),
				opts: platform.FindOptions{
					Offset: 2,
					Limit:  2,
				},
			},
			wants: wants{
				logEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("3"),
						Time:  time.Date(2009, time.November, 10, 3, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("4"),
						Time:  time.Date(2009, time.November, 10, 4, 0, 0, 0, time.UTC),
					},
				},
			},
		},
		{
			name: "descending log entries with offset and limit",
			fields: KeyValueLogFields{
				LogEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("1"),
						Time:  time.Date(2009, time.November, 10, 1, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("2"),
						Time:  time.Date(2009, time.November, 10, 2, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("3"),
						Time:  time.Date(2009, time.November, 10, 3, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("4"),
						Time:  time.Date(2009, time.November, 10, 4, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("5"),
						Time:  time.Date(2009, time.November, 10, 5, 0, 0, 0, time.UTC),
					},
				},
			},
			args: args{
				key: []byte("abc"),
				opts: platform.FindOptions{
					Offset:     2,
					Limit:      2,
					Descending: true,
				},
			},
			wants: wants{
				logEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("3"),
						Time:  time.Date(2009, time.November, 10, 3, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("2"),
						Time:  time.Date(2009, time.November, 10, 2, 0, 0, 0, time.UTC),
					},
				},
			},
		},
		{
			name: "offset exceeds log range",
			fields: KeyValueLogFields{
				LogEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("1"),
						Time:  time.Date(2009, time.November, 10, 1, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("2"),
						Time:  time.Date(2009, time.November, 10, 2, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("3"),
						Time:  time.Date(2009, time.November, 10, 3, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("4"),
						Time:  time.Date(2009, time.November, 10, 4, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("5"),
						Time:  time.Date(2009, time.November, 10, 5, 0, 0, 0, time.UTC),
					},
				},
			},
			args: args{
				key: []byte("abc"),
				opts: platform.FindOptions{
					Offset: 5,
				},
			},
			wants: wants{
				logEntries: []LogEntry{},
			},
		},
		{
			name: "offset exceeds log range descending",
			fields: KeyValueLogFields{
				LogEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("1"),
						Time:  time.Date(2009, time.November, 10, 1, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("2"),
						Time:  time.Date(2009, time.November, 10, 2, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("3"),
						Time:  time.Date(2009, time.November, 10, 3, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("4"),
						Time:  time.Date(2009, time.November, 10, 4, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("5"),
						Time:  time.Date(2009, time.November, 10, 5, 0, 0, 0, time.UTC),
					},
				},
			},
			args: args{
				key: []byte("abc"),
				opts: platform.FindOptions{
					Offset:     5,
					Descending: true,
				},
			},
			wants: wants{
				logEntries: []LogEntry{},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, done := init(tt.fields, t)
			defer done()
			ctx := context.Background()
			logEntries := []LogEntry{}
			err := s.ForEachLogEntry(ctx, tt.args.key, tt.args.opts, func(v []byte, t time.Time) error {
				logEntries = append(logEntries, LogEntry{
					Key:   tt.args.key,
					Value: v,
					Time:  t,
				})
				return nil
			})
			if (err != nil) != (tt.wants.err != nil) {
				t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)
			}

			if err != nil && tt.wants.err != nil {
				if err.Error() != tt.wants.err.Error() {
					t.Fatalf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error())
				}
			}

			if diff := cmp.Diff(logEntries, tt.wants.logEntries, keyValueLogCmpOptions...); diff != "" {
				t.Errorf("logEntries are different -got/+want\ndiff %s", diff)
			}
		})
	}
}

// FirstLogEntry tests the FirstLogEntry method for the KeyValueLog contract.
func FirstLogEntry(
	init func(KeyValueLogFields, *testing.T) (platform.KeyValueLog, func()),
	t *testing.T,
) {
	type args struct {
		key []byte
	}
	type wants struct {
		err      error
		logEntry LogEntry
	}

	tests := []struct {
		name   string
		fields KeyValueLogFields
		args   args
		wants  wants
	}{
		{
			name: "get first log entry",
			fields: KeyValueLogFields{
				LogEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("1"),
						Time:  time.Date(2009, time.November, 10, 1, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("2"),
						Time:  time.Date(2009, time.November, 10, 2, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("3"),
						Time:  time.Date(2009, time.November, 10, 3, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("4"),
						Time:  time.Date(2009, time.November, 10, 4, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("5"),
						Time:  time.Date(2009, time.November, 10, 5, 0, 0, 0, time.UTC),
					},
				},
			},
			args: args{
				key: []byte("abc"),
			},
			wants: wants{
				logEntry: LogEntry{
					Key:   []byte("abc"),
					Value: []byte("1"),
					Time:  time.Date(2009, time.November, 10, 1, 0, 0, 0, time.UTC),
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, done := init(tt.fields, t)
			defer done()
			ctx := context.Background()
			var err error
			logEntry := LogEntry{Key: tt.args.key}
			logEntry.Value, logEntry.Time, err = s.FirstLogEntry(ctx, tt.args.key)
			if (err != nil) != (tt.wants.err != nil) {
				t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)
			}

			if err != nil && tt.wants.err != nil {
				if err.Error() != tt.wants.err.Error() {
					t.Fatalf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error())
				}
			}

			if diff := cmp.Diff(logEntry, tt.wants.logEntry, keyValueLogCmpOptions...); diff != "" {
				t.Errorf("logEntries are different -got/+want\ndiff %s", diff)
			}
		})
	}
}

// LastLogEntry tests the LastLogEntry method for the KeyValueLog contract.
func LastLogEntry(
	init func(KeyValueLogFields, *testing.T) (platform.KeyValueLog, func()),
	t *testing.T,
) {
	type args struct {
		key []byte
	}
	type wants struct {
		err      error
		logEntry LogEntry
	}

	tests := []struct {
		name   string
		fields KeyValueLogFields
		args   args
		wants  wants
	}{
		{
			name: "get last log entry",
			fields: KeyValueLogFields{
				LogEntries: []LogEntry{
					{
						Key:   []byte("abc"),
						Value: []byte("1"),
						Time:  time.Date(2009, time.November, 10, 1, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("2"),
						Time:  time.Date(2009, time.November, 10, 2, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("3"),
						Time:  time.Date(2009, time.November, 10, 3, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("4"),
						Time:  time.Date(2009, time.November, 10, 4, 0, 0, 0, time.UTC),
					},
					{
						Key:   []byte("abc"),
						Value: []byte("5"),
						Time:  time.Date(2009, time.November, 10, 5, 0, 0, 0, time.UTC),
					},
				},
			},
			args: args{
				key: []byte("abc"),
			},
			wants: wants{
				logEntry: LogEntry{
					Key:   []byte("abc"),
					Value: []byte("5"),
					Time:  time.Date(2009, time.November, 10, 5, 0, 0, 0, time.UTC),
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, done := init(tt.fields, t)
			defer done()
			ctx := context.Background()
			var err error
			logEntry := LogEntry{Key: tt.args.key}
			logEntry.Value, logEntry.Time, err = s.LastLogEntry(ctx, tt.args.key)
			if (err != nil) != (tt.wants.err != nil) {
				t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)
			}

			if err != nil && tt.wants.err != nil {
				if err.Error() != tt.wants.err.Error() {
					t.Fatalf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error())
				}
			}

			if diff := cmp.Diff(logEntry, tt.wants.logEntry, keyValueLogCmpOptions...); diff != "" {
				t.Errorf("logEntries are different -got/+want\ndiff %s", diff)
			}
		})
	}
}
