// Copyright 2022 Google LLC
//
// Licensed 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
//
// https://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.

package orchestrator

import (
	"errors"
	"sort"
	"sync"
	"testing"
	"time"

	apiv1 "github.com/google/android-cuttlefish/frontend/src/host_orchestrator/api/v1"

	"github.com/google/go-cmp/cmp"
)

func TestMapOMNewOperation(t *testing.T) {
	om := NewMapOM()

	op := om.New()

	if op.Name == "" {
		t.Error("expected non empty value")
	}
	if op.Done {
		t.Error("expected false")
	}
}

func TestMapOMNewOperationNoNewUniqueUUIDsPanic(t *testing.T) {
	defer func() { _ = recover() }()
	om := NewMapOM()
	om.uuidFactory = func() string { return "sameuuid" }

	om.New()
	om.New()

	t.Error("did not panic")
}

func TestMapOMGetOperation(t *testing.T) {
	t.Run("exists", func(t *testing.T) {
		om := NewMapOM()
		newOp := om.New()

		op, err := om.Get(newOp.Name)

		if err != nil {
			t.Errorf("expected no error")
		}
		if op.Name != newOp.Name {
			t.Errorf("expected <<%q>>, got %q", newOp.Name, op.Name)
		}
	})

	t.Run("does not exist", func(t *testing.T) {
		om := NewMapOM()

		op, err := om.Get("foo")

		if notFoundErr, ok := err.(NotFoundOperationError); !ok {
			t.Errorf("expected <<%T>>, got %T", notFoundErr, err)
		}

		if (op != apiv1.Operation{}) {
			t.Errorf("expected zero value %T", op)
		}
	})
}

func TestMapOMCompleteOperation(t *testing.T) {
	result := &OperationResult{Error: errors.New("error")}

	t.Run("exists", func(t *testing.T) {
		om := NewMapOM()
		newOp := om.New()

		err := om.Complete(newOp.Name, result)

		if err != nil {
			t.Error("expected no error")
		}
		op, err := om.Get(newOp.Name)
		if err != nil {
			t.Error("expected no error")
		}
		if !op.Done {
			t.Error("expected true")
		}
	})

	t.Run("does not exist", func(t *testing.T) {
		om := NewMapOM()

		err := om.Complete("foo", result)

		if err == nil {
			t.Error("expected error")
		}
	})
}

func TestMapOMWaitOperation(t *testing.T) {
	dt := 1 * time.Second
	result := &OperationResult{Value: "foo"}

	t.Run("operation was completed", func(t *testing.T) {
		var wg sync.WaitGroup
		om := NewMapOM()
		newOp := om.New()
		// Testing more than one Wait call at the same time.
		for i := 0; i < 10; i++ {
			wg.Add(1)
			go func() {
				defer wg.Done()

				start := time.Now()
				got, err := om.Wait(newOp.Name, dt)
				duration := time.Since(start)

				if err != nil {
					t.Errorf("expected nil error, got %+v", err)
				}
				if duration >= dt {
					t.Error("reached the wait deadline")
				}
				if diff := cmp.Diff(result.Value, got.Value); diff != "" {
					t.Errorf("result value mismatch (-want +got):\n%s", diff)
				}
			}()
		}
		om.Complete(newOp.Name, result)
		wg.Wait()
	})

	t.Run("operation is not completed and deadline is reached", func(t *testing.T) {
		var wg sync.WaitGroup
		om := NewMapOM()
		newOp := om.New()
		// Testing more than one Wait call at the same time.
		for i := 0; i < 10; i++ {
			wg.Add(1)
			go func() {
				defer wg.Done()

				start := time.Now()
				res, err := om.Wait(newOp.Name, dt)
				duration := time.Since(start)

				if duration < dt {
					t.Error("wait deadline was not reached")
				}
				if timedOutErr, ok := err.(*OperationWaitTimeoutError); !ok {
					t.Errorf("expected <<%T>>, got %T", timedOutErr, err)
				}
				if res != nil {
					t.Error("expected nil result")
				}
			}()
		}
		wg.Wait()
	})

	t.Run("operation does not exist", func(t *testing.T) {
		om := NewMapOM()

		start := time.Now()
		res, err := om.Wait("foo", dt)
		duration := time.Since(start)

		if duration >= dt {
			t.Error("reached the wait deadline")
		}
		if notFoundErr, ok := err.(NotFoundOperationError); !ok {
			t.Errorf("expected <<%T>>, got %T", notFoundErr, err)
		}
		if res != nil {
			t.Error("expected nil result")
		}
	})
}

func TestMapOMGetOperationResult(t *testing.T) {
	t.Run("does not exist", func(t *testing.T) {
		om := NewMapOM()

		_, err := om.GetResult("foo")

		if notFoundErr, ok := err.(NotFoundOperationError); !ok {
			t.Errorf("expected <<%T>>, got %T", notFoundErr, err)
		}
	})

	t.Run("exists not done", func(t *testing.T) {
		om := NewMapOM()
		op := om.New()

		_, err := om.GetResult(op.Name)

		if notDoneErr, ok := err.(OperationNotDoneError); !ok {
			t.Errorf("expected <<%T>>, got %T", notDoneErr, err)
		}
	})

	t.Run("exists done", func(t *testing.T) {
		om := NewMapOM()
		op := om.New()
		om.Complete(op.Name, &OperationResult{Value: "bar"})

		res, err := om.GetResult(op.Name)

		if err != nil {
			t.Errorf("expected no error")
		}
		if res.Error != nil {
			t.Errorf("expected no error")
		}
		if diff := cmp.Diff("bar", res.Value); diff != "" {
			t.Errorf("result value (-want +got):\n%s", diff)
		}
	})
}

func TestMapOMListRunningEmpty(t *testing.T) {
	om := NewMapOM()

	result := om.ListRunning()

	if len(result) != 0 {
		t.Error("expected empty slice")
	}
}

func TestMapOMListRunning(t *testing.T) {
	om := NewMapOM()
	op := om.New()
	om.Complete(op.Name, &OperationResult{})
	op = om.New()
	om.Complete(op.Name, &OperationResult{})
	op1 := om.New()
	op2 := om.New()

	result := om.ListRunning()

	if len(result) != 2 {
		t.Error("expected 2 operations")
	}
	expected := []string{op1.Name, op2.Name}
	sort.Strings(expected)
	names := []string{}
	for _, s := range result {
		names = append(names, s.Name)
	}
	sort.Strings(names)

	if diff := cmp.Diff(expected, names); diff != "" {
		t.Errorf("result mismatch (-want +got):\n%s", diff)
	}

}
