/*
Copyright 2017 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 duck

import (
	"errors"
	"testing"

	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
)

func TestMatches(t *testing.T) {
	tests := []struct {
		name     string
		instance interface{}
		iface    Implementable
	}{{
		name:     "foo matches fooable",
		instance: &Foo{},
		iface:    &Fooable{},
	}, {
		name:     "bar matches barable",
		instance: &Bar{},
		iface:    &Barable{},
	}, {
		name:     "slice matches sliceable",
		instance: &Slice{},
		iface:    &Sliceable{},
	}, {
		name:     "string matches stringable",
		instance: &String{},
		iface:    &emptyStringable,
	}, {
		name: "other matches foo",
		instance: &struct {
			Status struct {
				Fooable *Fooable `json:"fooable,omitempty"`
			} `json:"status,omitempty"`
		}{},
		iface: &Fooable{},
	}, {
		name: "other (all) matches fooable",
		instance: &struct {
			Status struct {
				Fooable    *Fooable    `json:"fooable,omitempty"`
				Barable    *Barable    `json:"barable,omitempty"`
				Sliceable  Sliceable   `json:"sliceable,omitempty"`
				Stringable *Stringable `json:"stringable,omitempty"`
			} `json:"status,omitempty"`
		}{},
		iface: &Fooable{},
	}, {
		name: "other (all) matches barable",
		instance: &struct {
			Status struct {
				Fooable    *Fooable    `json:"fooable,omitempty"`
				Barable    *Barable    `json:"barable,omitempty"`
				Sliceable  Sliceable   `json:"sliceable,omitempty"`
				Stringable *Stringable `json:"stringable,omitempty"`
			} `json:"status,omitempty"`
		}{},
		iface: &Barable{},
	}, {
		name: "other (all) matches sliceable",
		instance: &struct {
			Status struct {
				Fooable    *Fooable    `json:"fooable,omitempty"`
				Barable    *Barable    `json:"barable,omitempty"`
				Sliceable  Sliceable   `json:"sliceable,omitempty"`
				Stringable *Stringable `json:"stringable,omitempty"`
			} `json:"status,omitempty"`
		}{},
		iface: &Sliceable{},
	}, {
		name: "other (all) matches stringable",
		instance: &struct {
			Status struct {
				Fooable    *Fooable    `json:"fooable,omitempty"`
				Barable    *Barable    `json:"barable,omitempty"`
				Sliceable  Sliceable   `json:"sliceable,omitempty"`
				Stringable *Stringable `json:"stringable,omitempty"`
			} `json:"status,omitempty"`
		}{},
		iface: &emptyStringable,
	}}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if err := VerifyType(test.instance, test.iface); err != nil {
				t.Error(err)
			}

			ok, err := ConformsToType(test.instance, test.iface)
			if err != nil {
				t.Error(err)
			}

			if !ok {
				t.Errorf("Expected %T to conform to %T", test.instance, test.iface)
			}
		})
	}
}

func TestMismatches(t *testing.T) {
	tests := []struct {
		name     string
		instance interface{}
		iface    Implementable
	}{{
		name:     "foo doesn't match barable",
		instance: &Foo{},
		iface:    &Barable{},
	}, {
		name:     "bar doesn't match fooable",
		instance: &Bar{},
		iface:    &Fooable{},
	}, {
		name:     "foo doesn't match sliceable",
		instance: &Foo{},
		iface:    &Sliceable{},
	}, {
		name: "other matches neither (foo)",
		instance: &struct {
			Status struct {
				Done bool `json:"done,omitempty"`
			} `json:"status,omitempty"`
		}{},
		iface: &Fooable{},
	}, {
		name: "other matches neither (slice)",
		instance: &struct {
			Status struct {
				Done bool `json:"done,omitempty"`
			} `json:"status,omitempty"`
		}{},
		iface: &Sliceable{},
	}}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if err := VerifyType(test.instance, test.iface); err == nil {
				t.Errorf("Unexpected success %T implements %T", test.instance, test.iface)
			}

			ok, err := ConformsToType(test.instance, test.iface)
			if err != nil {
				t.Error(err)
			}

			if ok {
				t.Errorf("Expected %T to not conform to %T", test.instance, test.iface)
			}
		})
	}
}

func TestErrors(t *testing.T) {
	tests := []struct {
		name     string
		instance interface{}
		iface    Implementable
	}{{
		name:     "duck type - fails to marshal",
		instance: &Foo{},
		iface:    &UnableToMarshal{},
	}, {
		name:     "duck type - fails to unmarshal",
		instance: &Foo{},
		iface:    &UnableToUnmarshal{},
	}, {
		name:     "instance - fails to unmarshal",
		instance: &UnableToUnmarshal{},
		iface:    &Fooable{},
	}, {
		name:     "instance - fails to marshal",
		instance: &UnableToMarshal{},
		iface:    &Fooable{},
	}, {
		name:     "duck type - unexported fields",
		instance: &Foo{},
		iface:    &UnexportedFields{},
	}}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if err := VerifyType(test.instance, test.iface); err == nil {
				t.Error("expected VerifyType to return an error")
			}

			if _, err := ConformsToType(test.instance, test.iface); err == nil {
				t.Error("expected ConformsToType to return an error")
			}
		})
	}
}

// Define a "Fooable" duck type.
type Fooable struct {
	Field1 string `json:"field1,omitempty"`
	Field2 string `json:"field2,omitempty"`
}
type Foo struct {
	Status FooStatus `json:"status"`
}
type FooStatus struct {
	Fooable *Fooable `json:"fooable,omitempty"`
}

var (
	_ Implementable = (*Fooable)(nil)
	_ Populatable   = (*Foo)(nil)
)

func (*Foo) GetObjectKind() schema.ObjectKind {
	return nil // not used
}

func (*Foo) DeepCopyObject() runtime.Object {
	return nil // not used
}

func (*Foo) GetListType() runtime.Object {
	return nil // not used
}

func (*Fooable) GetFullType() Populatable {
	return &Foo{}
}

func (f *Foo) Populate() {
	f.Status.Fooable = &Fooable{
		// Populate ALL fields
		Field1: "foo",
		Field2: "bar",
	}
}

// Define a "Barable" duck type.
type Barable struct {
	Field1 int  `json:"field1,omitempty"`
	Field2 bool `json:"field2,omitempty"`
}
type Bar struct {
	Status BarStatus `json:"status"`
}
type BarStatus struct {
	Barable *Barable `json:"barable,omitempty"`
}

var (
	_ Implementable = (*Barable)(nil)
	_ Populatable   = (*Bar)(nil)
)

func (*Bar) GetObjectKind() schema.ObjectKind {
	return nil // not used
}

func (*Bar) DeepCopyObject() runtime.Object {
	return nil // not used
}

func (*Bar) GetListType() runtime.Object {
	return nil // not used
}

func (*Barable) GetFullType() Populatable {
	return &Bar{}
}

func (f *Bar) Populate() {
	f.Status.Barable = &Barable{
		// Populate ALL fields
		Field1: 42,
		Field2: true,
	}
}

// Define a "Sliceable" duck type.
type AStruct struct {
	Field string `json:"field,omitempty"`
}
type (
	Sliceable []AStruct
	Slice     struct {
		Status SliceStatus `json:"status"`
	}
)

type SliceStatus struct {
	Sliceable *Sliceable `json:"sliceable,omitempty"`
}

var (
	_ Implementable = (*Sliceable)(nil)
	_ Populatable   = (*Slice)(nil)
)

func (*Slice) GetObjectKind() schema.ObjectKind {
	return nil // not used
}

func (*Slice) DeepCopyObject() runtime.Object {
	return nil // not used
}

func (*Slice) GetListType() runtime.Object {
	return nil // not used
}

func (*Sliceable) GetFullType() Populatable {
	return &Slice{}
}

func (f *Slice) Populate() {
	f.Status.Sliceable = &Sliceable{{"foo"}, {"bar"}}
}

// Define a "Stringable" duck type.
type (
	Stringable string
	String     struct {
		Status StringStatus `json:"status"`
	}
)

type StringStatus struct {
	Stringable Stringable `json:"stringable,omitempty"`
}

var (
	_ Implementable = (*Stringable)(nil)
	_ Populatable   = (*String)(nil)
)

func (*String) GetObjectKind() schema.ObjectKind {
	return nil // not used
}

func (*String) DeepCopyObject() runtime.Object {
	return nil // not used
}

func (*String) GetListType() runtime.Object {
	return nil // not used
}

func (*Stringable) GetFullType() Populatable {
	return &String{}
}

func (f *String) Populate() {
	f.Status.Stringable = Stringable("hello duck")
}

// We have to do this for Stringable because we're aliasing a value type.
var emptyStringable Stringable

// For testing this doubles as the 'Implementable'
// and 'Populatable'
type UnableToMarshal struct{}

var (
	_ Implementable = (*UnableToMarshal)(nil)
	_ Populatable   = (*UnableToMarshal)(nil)
)

func (*UnableToMarshal) GetObjectKind() schema.ObjectKind {
	return nil // not used
}

func (*UnableToMarshal) DeepCopyObject() runtime.Object {
	return nil // not used
}

func (*UnableToMarshal) GetListType() runtime.Object {
	return nil // not used
}

func (u *UnableToMarshal) GetFullType() Populatable {
	return u
}

func (*UnableToMarshal) Populate() {
}

func (*UnableToMarshal) MarshalJSON() ([]byte, error) {
	return nil, errors.New("I will never marshal for you")
}

// For testing this doubles as the 'Implementable'
// and 'Populatable'
type UnableToUnmarshal struct{}

var (
	_ Implementable = (*UnableToUnmarshal)(nil)
	_ Populatable   = (*UnableToUnmarshal)(nil)
)

func (*UnableToUnmarshal) GetObjectKind() schema.ObjectKind {
	return nil // not used
}

func (*UnableToUnmarshal) DeepCopyObject() runtime.Object {
	return nil // not used
}

func (*UnableToUnmarshal) GetListType() runtime.Object {
	return nil // not used
}

func (u *UnableToUnmarshal) GetFullType() Populatable {
	return u
}

func (*UnableToUnmarshal) Populate() {
}

func (*UnableToUnmarshal) UnmarshalJSON([]byte) error {
	return errors.New("I will never unmarshal for you")
}

// For testing this doubles as the 'Implementable'
// and 'Populatable'
type UnexportedFields struct {
	a string
}

var (
	_ Implementable = (*UnexportedFields)(nil)
	_ Populatable   = (*UnexportedFields)(nil)
)

func (*UnexportedFields) GetObjectKind() schema.ObjectKind {
	return nil // not used
}

func (*UnexportedFields) DeepCopyObject() runtime.Object {
	return nil // not used
}

func (*UnexportedFields) GetListType() runtime.Object {
	return nil // not used
}

func (*UnexportedFields) GetFullType() Populatable {
	return &UnexportedFields{}
}

func (u *UnexportedFields) Populate() {
	u.a = "hello"
}
