// Copyright © 2019 The Knative Authors
//
// 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
//
//     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.

package v1

import (
	"testing"
	"time"

	"gotest.tools/assert"
	"k8s.io/apimachinery/pkg/fields"
	"k8s.io/apimachinery/pkg/labels"
	servingv1 "knative.dev/serving/pkg/apis/serving/v1"

	"knative.dev/client/pkg/util/mock"
	"knative.dev/client/pkg/wait"
)

type MockKnServingClient struct {
	t         *testing.T
	recorder  *ServingRecorder
	namespace string
}

// NewMockKnServiceClient returns a new mock instance which you need to record for
func NewMockKnServiceClient(t *testing.T, ns ...string) *MockKnServingClient {
	namespace := "default"
	if len(ns) > 0 {
		namespace = ns[0]
	}
	return &MockKnServingClient{
		t:        t,
		recorder: &ServingRecorder{mock.NewRecorder(t, namespace)},
	}
}

// recorder for service
type ServingRecorder struct {
	r *mock.Recorder
}

// Get the record to start for the recorder
func (c *MockKnServingClient) Recorder() *ServingRecorder {
	return c.recorder
}

// Namespace of this client
func (c *MockKnServingClient) Namespace() string {
	return c.recorder.r.Namespace()
}

// Get Service
func (sr *ServingRecorder) GetService(name interface{}, service *servingv1.Service, err error) {
	sr.r.Add("GetService", []interface{}{name}, []interface{}{service, err})
}

func (c *MockKnServingClient) GetService(name string) (*servingv1.Service, error) {
	call := c.recorder.r.VerifyCall("GetService", name)
	return call.Result[0].(*servingv1.Service), mock.ErrorOrNil(call.Result[1])
}

// List services
func (sr *ServingRecorder) ListServices(opts interface{}, serviceList *servingv1.ServiceList, err error) {
	sr.r.Add("ListServices", []interface{}{opts}, []interface{}{serviceList, err})
}

func (c *MockKnServingClient) ListServices(opts ...ListConfig) (*servingv1.ServiceList, error) {
	call := c.recorder.r.VerifyCall("ListServices", opts)
	return call.Result[0].(*servingv1.ServiceList), mock.ErrorOrNil(call.Result[1])
}

// Create a new service
func (sr *ServingRecorder) CreateService(service interface{}, err error) {
	sr.r.Add("CreateService", []interface{}{service}, []interface{}{err})
}

func (c *MockKnServingClient) CreateService(service *servingv1.Service) error {
	call := c.recorder.r.VerifyCall("CreateService", service)
	return mock.ErrorOrNil(call.Result[0])
}

// Update the given service
func (sr *ServingRecorder) UpdateService(service interface{}, err error) {
	sr.r.Add("UpdateService", []interface{}{service}, []interface{}{err})
}

func (c *MockKnServingClient) UpdateService(service *servingv1.Service) error {
	call := c.recorder.r.VerifyCall("UpdateService", service)
	return mock.ErrorOrNil(call.Result[0])
}

// Delegate to shared retry method
func (c *MockKnServingClient) UpdateServiceWithRetry(name string, updateFunc serviceUpdateFunc, maxRetry int) error {
	return updateServiceWithRetry(c, name, updateFunc, maxRetry)
}

// Delete a service by name
func (sr *ServingRecorder) DeleteService(name, timeout interface{}, err error) {
	sr.r.Add("DeleteService", []interface{}{name, timeout}, []interface{}{err})
}

func (c *MockKnServingClient) DeleteService(name string, timeout time.Duration) error {
	call := c.recorder.r.VerifyCall("DeleteService", name, timeout)
	return mock.ErrorOrNil(call.Result[0])
}

// Wait for a service to become ready, but not longer than provided timeout
func (sr *ServingRecorder) WaitForService(name interface{}, timeout interface{}, callback interface{}, err error, duration time.Duration) {
	sr.r.Add("WaitForService", []interface{}{name, timeout, callback}, []interface{}{err, duration})
}

func (c *MockKnServingClient) WaitForService(name string, timeout time.Duration, msgCallback wait.MessageCallback) (error, time.Duration) {
	call := c.recorder.r.VerifyCall("WaitForService", name, timeout, msgCallback)
	return mock.ErrorOrNil(call.Result[0]), call.Result[1].(time.Duration)
}

// Get a revision by name
func (sr *ServingRecorder) GetRevision(name interface{}, revision *servingv1.Revision, err error) {
	sr.r.Add("GetRevision", []interface{}{name}, []interface{}{revision, err})
}

func (c *MockKnServingClient) GetRevision(name string) (*servingv1.Revision, error) {
	call := c.recorder.r.VerifyCall("GetRevision", name)
	return call.Result[0].(*servingv1.Revision), mock.ErrorOrNil(call.Result[1])
}

// List revisions
func (sr *ServingRecorder) ListRevisions(opts interface{}, revisionList *servingv1.RevisionList, err error) {
	sr.r.Add("ListRevisions", []interface{}{opts}, []interface{}{revisionList, err})
}

func (c *MockKnServingClient) ListRevisions(opts ...ListConfig) (*servingv1.RevisionList, error) {
	call := c.recorder.r.VerifyCall("ListRevisions", opts)
	return call.Result[0].(*servingv1.RevisionList), mock.ErrorOrNil(call.Result[1])
}

// Delete a revision
func (sr *ServingRecorder) DeleteRevision(name, timeout interface{}, err error) {
	sr.r.Add("DeleteRevision", []interface{}{name, timeout}, []interface{}{err})
}

func (c *MockKnServingClient) DeleteRevision(name string, timeout time.Duration) error {
	call := c.recorder.r.VerifyCall("DeleteRevision", name, timeout)
	return mock.ErrorOrNil(call.Result[0])
}

// Get a route by its unique name
func (sr *ServingRecorder) GetRoute(name interface{}, route *servingv1.Route, err error) {
	sr.r.Add("GetRoute", []interface{}{name}, []interface{}{route, err})
}

func (c *MockKnServingClient) GetRoute(name string) (*servingv1.Route, error) {
	call := c.recorder.r.VerifyCall("GetRoute", name)
	return call.Result[0].(*servingv1.Route), mock.ErrorOrNil(call.Result[1])

}

// List routes
func (sr *ServingRecorder) ListRoutes(opts interface{}, routeList *servingv1.RouteList, err error) {
	sr.r.Add("ListRoutes", []interface{}{opts}, []interface{}{routeList, err})
}

func (c *MockKnServingClient) ListRoutes(opts ...ListConfig) (*servingv1.RouteList, error) {
	call := c.recorder.r.VerifyCall("ListRoutes", opts)
	return call.Result[0].(*servingv1.RouteList), mock.ErrorOrNil(call.Result[1])
}

// GetConfiguration records a call to GetConfiguration with possible return values
func (sr *ServingRecorder) GetConfiguration(name string, config *servingv1.Configuration, err error) {
	sr.r.Add("GetConfiguration", []interface{}{name}, []interface{}{config, err})

}

// Check for the base reviision
func (c *MockKnServingClient) GetBaseRevision(service *servingv1.Service) (*servingv1.Revision, error) {
	return getBaseRevision(c, service)
}

// GetConfiguration returns a configuration looked up by name
func (c *MockKnServingClient) GetConfiguration(name string) (*servingv1.Configuration, error) {
	call := c.recorder.r.VerifyCall("GetConfiguration", name)
	return call.Result[0].(*servingv1.Configuration), mock.ErrorOrNil(call.Result[1])
}

// Check that every recorded method has been called
func (sr *ServingRecorder) Validate() {
	sr.r.CheckThatAllRecordedMethodsHaveBeenCalled()
}

// HasLabelSelector returns a comparable which can be used for asserting that list methods are called
// with the appropriate label selector
func HasLabelSelector(keyAndValues ...string) func(t *testing.T, a interface{}) {
	return func(t *testing.T, a interface{}) {
		lc := a.([]ListConfig)
		listConfigCollector := listConfigCollector{
			Labels: make(labels.Set),
			Fields: make(fields.Set),
		}
		lc[0](&listConfigCollector)
		for i := 0; i < len(keyAndValues); i += 2 {
			assert.Equal(t, listConfigCollector.Labels[keyAndValues[i]], keyAndValues[i+1])
		}
	}
}

// HasFieldSelector returns a comparable which can be used for asserting that list methods are called
// with the appropriate field selectors
func HasFieldSelector(keyAndValues ...string) func(t *testing.T, a interface{}) {
	return func(t *testing.T, a interface{}) {
		lc := a.([]ListConfig)
		listConfigCollector := listConfigCollector{
			Labels: make(labels.Set),
			Fields: make(fields.Set),
		}
		lc[0](&listConfigCollector)
		for i := 0; i < len(keyAndValues); i += 2 {
			assert.Equal(t, listConfigCollector.Fields[keyAndValues[i]], keyAndValues[i+1])
		}
	}
}

// HasSelector returns a comparable which can be used for asserting that list methods are called
// with the appropriate label and field selectors
func HasSelector(labelKeysAndValues []string, fieldKeysAndValue []string) func(t *testing.T, a interface{}) {
	return func(t *testing.T, a interface{}) {
		HasLabelSelector(labelKeysAndValues...)(t, a)
		HasFieldSelector(fieldKeysAndValue...)(t, a)
	}
}
