// Copyright 2016-2018, Pulumi Corporation.
//
// 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.

// nolint: lll
package pulumi

import (
	"context"
	"errors"
	"fmt"
	"reflect"
	"testing"

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

func await(out Output) (interface{}, bool, bool, []Resource, error) {
	return out.getState().await(context.Background())
}

func assertApplied(t *testing.T, out Output) {
	_, known, _, _, err := await(out)
	assert.True(t, known)
	assert.Nil(t, err)
}

func newIntOutput() IntOutput {
	return IntOutput{newOutputState(nil, reflect.TypeOf(42))}
}

func TestBasicOutputs(t *testing.T) {
	// Just test basic resolve and reject functionality.
	{
		out, resolve, _ := NewOutput()
		go func() {
			resolve(42)
		}()
		v, known, secret, deps, err := await(out)
		assert.Nil(t, err)
		assert.True(t, known)
		assert.False(t, secret)
		assert.Nil(t, deps)
		assert.NotNil(t, v)
		assert.Equal(t, 42, v.(int))
	}
	{
		out, _, reject := NewOutput()
		go func() {
			reject(errors.New("boom"))
		}()
		v, _, _, _, err := await(out)
		assert.NotNil(t, err)
		assert.Nil(t, v)
	}
}

func TestArrayOutputs(t *testing.T) {
	out := ArrayOutput{newOutputState(nil, reflect.TypeOf([]interface{}{}))}
	go func() {
		out.resolve([]interface{}{nil, 0, "x"}, true, false, nil)
	}()
	{
		assertApplied(t, out.ApplyT(func(arr []interface{}) (interface{}, error) {
			assert.NotNil(t, arr)
			if assert.Equal(t, 3, len(arr)) {
				assert.Equal(t, nil, arr[0])
				assert.Equal(t, 0, arr[1])
				assert.Equal(t, "x", arr[2])
			}
			return nil, nil
		}))
	}
}

func TestBoolOutputs(t *testing.T) {
	out := BoolOutput{newOutputState(nil, reflect.TypeOf(false))}
	go func() {
		out.resolve(true, true, false, nil)
	}()
	{
		assertApplied(t, out.ApplyT(func(v bool) (interface{}, error) {
			assert.True(t, v)
			return nil, nil
		}))
	}
}

func TestMapOutputs(t *testing.T) {
	out := MapOutput{newOutputState(nil, reflect.TypeOf(map[string]interface{}{}))}
	go func() {
		out.resolve(map[string]interface{}{
			"x": 1,
			"y": false,
			"z": "abc",
		}, true, false, nil)
	}()
	{
		assertApplied(t, out.ApplyT(func(v map[string]interface{}) (interface{}, error) {
			assert.NotNil(t, v)
			assert.Equal(t, 1, v["x"])
			assert.Equal(t, false, v["y"])
			assert.Equal(t, "abc", v["z"])
			return nil, nil
		}))
	}
}

func TestNumberOutputs(t *testing.T) {
	out := Float64Output{newOutputState(nil, reflect.TypeOf(float64(0)))}
	go func() {
		out.resolve(42.345, true, false, nil)
	}()
	{
		assertApplied(t, out.ApplyT(func(v float64) (interface{}, error) {
			assert.Equal(t, 42.345, v)
			return nil, nil
		}))
	}
}

func TestStringOutputs(t *testing.T) {
	out := StringOutput{newOutputState(nil, reflect.TypeOf(""))}
	go func() {
		out.resolve("a stringy output", true, false, nil)
	}()
	{
		assertApplied(t, out.ApplyT(func(v string) (interface{}, error) {
			assert.Equal(t, "a stringy output", v)
			return nil, nil
		}))
	}
}

func TestResolveOutputToOutput(t *testing.T) {
	// Test that resolving an output to an output yields the value, not the output.
	{
		out, resolve, _ := NewOutput()
		go func() {
			other, resolveOther, _ := NewOutput()
			resolve(other)
			go func() { resolveOther(99) }()
		}()
		assertApplied(t, out.ApplyT(func(v interface{}) (interface{}, error) {
			assert.Equal(t, v, 99)
			return nil, nil
		}))
	}
	// Similarly, test that resolving an output to a rejected output yields an error.
	{
		out, resolve, _ := NewOutput()
		go func() {
			other, _, rejectOther := NewOutput()
			resolve(other)
			go func() { rejectOther(errors.New("boom")) }()
		}()
		v, _, _, _, err := await(out)
		assert.NotNil(t, err)
		assert.Nil(t, v)
	}
}

// Test that ToOutput works with a struct type.
func TestToOutputStruct(t *testing.T) {
	out := ToOutput(nestedTypeInputs{Foo: String("bar"), Bar: Int(42)})
	_, ok := out.(nestedTypeOutput)
	assert.True(t, ok)

	v, known, secret, deps, err := await(out)
	assert.True(t, known)
	assert.False(t, secret)
	assert.Nil(t, deps)
	assert.NoError(t, err)
	assert.Equal(t, nestedType{Foo: "bar", Bar: 42}, v)

	out = ToOutput(out)
	_, ok = out.(nestedTypeOutput)
	assert.True(t, ok)

	v, known, secret, deps, err = await(out)
	assert.True(t, known)
	assert.False(t, secret)
	assert.Nil(t, deps)

	assert.NoError(t, err)
	assert.Equal(t, nestedType{Foo: "bar", Bar: 42}, v)

	out = ToOutput(nestedTypeInputs{Foo: ToOutput(String("bar")).(StringInput), Bar: ToOutput(Int(42)).(IntInput)})
	_, ok = out.(nestedTypeOutput)
	assert.True(t, ok)

	v, known, secret, deps, err = await(out)
	assert.True(t, known)
	assert.False(t, secret)
	assert.Nil(t, deps)
	assert.NoError(t, err)
	assert.Equal(t, nestedType{Foo: "bar", Bar: 42}, v)
}

type arrayLenInput Array

func (arrayLenInput) ElementType() reflect.Type {
	return Array{}.ElementType()
}

func (i arrayLenInput) ToIntOutput() IntOutput {
	return i.ToIntOutputWithContext(context.Background())
}

func (i arrayLenInput) ToIntOutputWithContext(ctx context.Context) IntOutput {
	return ToOutput(i).ApplyT(func(arr []interface{}) int {
		return len(arr)
	}).(IntOutput)
}

func (i arrayLenInput) ToIntPtrOutput() IntPtrOutput {
	return i.ToIntPtrOutputWithContext(context.Background())
}

func (i arrayLenInput) ToIntPtrOutputWithContext(ctx context.Context) IntPtrOutput {
	return ToOutput(i).ApplyT(func(arr []interface{}) *int {
		v := len(arr)
		return &v
	}).(IntPtrOutput)
}

// Test that ToOutput converts inputs appropriately.
func TestToOutputConvert(t *testing.T) {
	out := ToOutput(nestedTypeInputs{Foo: ID("bar"), Bar: arrayLenInput{Int(42)}})
	_, ok := out.(nestedTypeOutput)
	assert.True(t, ok)

	v, known, secret, deps, err := await(out)
	assert.True(t, known)
	assert.False(t, secret)
	assert.Nil(t, deps)
	assert.NoError(t, err)
	assert.Equal(t, nestedType{Foo: "bar", Bar: 1}, v)
}

// Test that ToOutput correctly handles nested inputs and outputs when the argument is an input or interface{}.
func TestToOutputAny(t *testing.T) {
	type args struct {
		S StringInput
		I IntInput
		A Input
	}

	out := ToOutput(&args{
		S: ID("hello"),
		I: Int(42).ToIntOutput(),
		A: Map{"world": Bool(true).ToBoolOutput()},
	})
	_, ok := out.(AnyOutput)
	assert.True(t, ok)

	v, known, secret, deps, err := await(out)
	assert.True(t, known)
	assert.False(t, secret)
	assert.Nil(t, deps)
	assert.NoError(t, err)

	argsV := v.(*args)

	si, ok := argsV.S.(ID)
	assert.True(t, ok)
	assert.Equal(t, ID("hello"), si)

	io, ok := argsV.I.(IntOutput)
	assert.True(t, ok)
	assert.Equal(t, uint32(outputResolved), io.state)
	assert.Equal(t, 42, io.value)

	ai, ok := argsV.A.(Map)
	assert.True(t, ok)

	bo, ok := ai["world"].(BoolOutput)
	assert.True(t, ok)
	assert.Equal(t, uint32(outputResolved), bo.getState().state)
	assert.Equal(t, true, bo.value)
}

func TestToOutputAnyDeps(t *testing.T) {
	type args struct {
		S StringInput
		I IntInput
		A Input
		R Resource
	}

	stringDep1, stringDep2 := &ResourceState{}, &ResourceState{}
	stringOut := StringOutput{newOutputState(nil, reflect.TypeOf(""), stringDep1)}
	go func() {
		stringOut.resolve("a stringy output", true, false, []Resource{stringDep2})
	}()

	intDep1, intDep2 := &ResourceState{}, &ResourceState{}
	intOut := IntOutput{newOutputState(nil, reflect.TypeOf(0), intDep1)}
	go func() {
		intOut.resolve(42, true, false, []Resource{intDep2})
	}()

	boolDep1, boolDep2 := &ResourceState{}, &ResourceState{}
	boolOut := BoolOutput{newOutputState(nil, reflect.TypeOf(true), boolDep1)}
	go func() {
		boolOut.resolve(true, true, false, []Resource{boolDep2})
	}()

	res := &ResourceState{}

	out := ToOutput(&args{
		S: stringOut,
		I: intOut,
		A: Map{"world": boolOut},
		R: res,
	})
	_, ok := out.(AnyOutput)
	assert.True(t, ok)

	v, known, secret, deps, err := await(out)
	assert.True(t, known)
	assert.False(t, secret)
	assert.ElementsMatch(t, []Resource{stringDep1, stringDep2, intDep1, intDep2, boolDep1, boolDep2, res}, deps)
	assert.NoError(t, err)

	argsV := v.(*args)

	so, ok := argsV.S.(StringOutput)
	assert.True(t, ok)
	assert.Equal(t, uint32(outputResolved), so.state)
	assert.Equal(t, "a stringy output", so.value)
	assert.ElementsMatch(t, []Resource{stringDep1, stringDep2}, so.deps)

	io, ok := argsV.I.(IntOutput)
	assert.True(t, ok)
	assert.Equal(t, uint32(outputResolved), io.state)
	assert.Equal(t, 42, io.value)
	assert.ElementsMatch(t, []Resource{intDep1, intDep2}, io.deps)

	ai, ok := argsV.A.(Map)
	assert.True(t, ok)

	bo, ok := ai["world"].(BoolOutput)
	assert.True(t, ok)
	assert.Equal(t, uint32(outputResolved), bo.getState().state)
	assert.Equal(t, true, bo.value)
	assert.ElementsMatch(t, []Resource{boolDep1, boolDep2}, bo.deps)
}

type args struct {
	S string
	I int
	A interface{}
}

type argsInputs struct {
	S StringInput
	I IntInput
	A Input
}

func (*argsInputs) ElementType() reflect.Type {
	return reflect.TypeOf((*args)(nil))
}

// Test that ToOutput correctly handles nested inputs when the argument is an input with no corresponding output type.
func TestToOutputInputAny(t *testing.T) {
	out := ToOutput(&argsInputs{
		S: ID("hello"),
		I: Int(42),
		A: Map{"world": Bool(true).ToBoolOutput()},
	})
	_, ok := out.(AnyOutput)
	assert.True(t, ok)

	v, known, secret, deps, err := await(out)
	assert.True(t, known)
	assert.False(t, secret)
	assert.Nil(t, deps)
	assert.NoError(t, err)

	assert.Equal(t, &args{
		S: "hello",
		I: 42,
		A: map[string]interface{}{"world": true},
	}, v)
}

// Test that Unsecret will return an Output that has an unwrapped secret
func TestUnsecret(t *testing.T) {
	s := ToSecret(String("foo"))
	// assert that secret is immediately secret
	assert.True(t, IsSecret(s))

	unS := Unsecret(s)
	// assert that we do not have a secret
	assert.False(t, IsSecret(unS))

	errChan := make(chan error)
	resultChan := make(chan string)
	secretChan := make(chan bool)

	unS.ApplyT(func(v interface{}) (string, error) {
		// assert secretness after the output resolves
		secretChan <- IsSecret(unS)
		val := v.(string)
		if val == "foo" {
			// validate the value
			resultChan <- val
		} else {
			errChan <- fmt.Errorf("Invalid result: %v", val)
		}
		return val, nil
	})

	for i := 0; i < 2; i++ {
		select {
		case err := <-errChan:
			assert.Nil(t, err)
			break
		case r := <-resultChan:
			assert.Equal(t, "foo", r)
			break
		case isSecret := <-secretChan:
			assert.False(t, isSecret)
			break
		}
	}
}

// Test that SecretT sets appropriate internal state and that IsSecret appropriately reads it.
func TestSecrets(t *testing.T) {
	s := ToSecret(String("foo"))
	// assert that secret is immediately secret
	assert.True(t, IsSecret(s))

	errChan := make(chan error)
	resultChan := make(chan string)
	secretChan := make(chan bool)

	s.ApplyT(func(v interface{}) (string, error) {
		// assert secretness after the output resolves
		secretChan <- IsSecret(s)
		val := v.(string)
		if val == "foo" {
			// validate the value
			resultChan <- val
		} else {
			errChan <- fmt.Errorf("Invalid result: %v", val)
		}
		return val, nil
	})

	for i := 0; i < 2; i++ {
		select {
		case err := <-errChan:
			assert.Nil(t, err)
			break
		case r := <-resultChan:
			assert.Equal(t, "foo", r)
			break
		case isSecret := <-secretChan:
			assert.True(t, isSecret)
			break
		}
	}

}

// Test that secretness is properly bubbled up with all/apply.
func TestSecretApply(t *testing.T) {
	s1 := ToSecret(String("foo"))
	// assert that secret is immediately secret
	assert.True(t, IsSecret(s1))
	s2 := StringInput(String("bar"))

	errChan := make(chan error)
	resultChan := make(chan string)
	secretChan := make(chan bool)

	s := All(s1, s2).ApplyT(func(v interface{}) (string, error) {
		val := v.([]interface{})
		return val[0].(string) + val[1].(string), nil
	})
	s.ApplyT(func(v interface{}) (string, error) {
		// assert secretness after the output resolves
		secretChan <- IsSecret(s)
		val := v.(string)
		if val == "foobar" {
			// validate the value
			resultChan <- val
		} else {
			errChan <- fmt.Errorf("Invalid result: %v", val)
		}
		return val, nil
	})

	for i := 0; i < 2; i++ {
		select {
		case err := <-errChan:
			assert.Nil(t, err)
			break
		case r := <-resultChan:
			assert.Equal(t, "foobar", r)
			break
		case isSecret := <-secretChan:
			assert.True(t, isSecret)
			break
		}
	}

}

func TestNil(t *testing.T) {
	ao := Any(nil)
	v, known, secret, deps, err := await(ao)
	assert.True(t, known)
	assert.False(t, secret)
	assert.Nil(t, deps)
	assert.NoError(t, err)
	assert.Equal(t, nil, v)

	o := ToOutput(nil)
	v, known, secret, deps, err = await(o)
	assert.True(t, known)
	assert.False(t, secret)
	assert.Nil(t, deps)
	assert.NoError(t, err)
	assert.Equal(t, nil, v)

	o = ToOutput(ao)
	v, known, secret, deps, err = await(o)
	assert.True(t, known)
	assert.False(t, secret)
	assert.Nil(t, deps)
	assert.NoError(t, err)
	assert.Equal(t, nil, v)

	ao = ToOutput("").ApplyT(func(v string) interface{} {
		return nil
	}).(AnyOutput)
	v, known, secret, deps, err = await(ao)
	assert.True(t, known)
	assert.False(t, secret)
	assert.Nil(t, deps)
	assert.NoError(t, err)
	assert.Equal(t, nil, v)

	bo := ao.ApplyT(func(x interface{}) bool {
		return x == nil
	})
	v, known, secret, deps, err = await(bo)
	assert.True(t, known)
	assert.False(t, secret)
	assert.Nil(t, deps)
	assert.NoError(t, err)
	assert.Equal(t, true, v)
}

// Test that dependencies flow through all/apply.
func TestDeps(t *testing.T) {
	stringDep1, stringDep2 := &ResourceState{}, &ResourceState{}
	stringOut := StringOutput{newOutputState(nil, reflect.TypeOf(""), stringDep1)}
	assert.ElementsMatch(t, []Resource{stringDep1}, stringOut.deps)
	go func() {
		stringOut.resolve("hello", true, false, []Resource{stringDep2})
	}()

	boolDep1, boolDep2 := &ResourceState{}, &ResourceState{}
	boolOut := BoolOutput{newOutputState(nil, reflect.TypeOf(true), boolDep1)}
	assert.ElementsMatch(t, []Resource{boolDep1}, boolOut.deps)
	go func() {
		boolOut.resolve(true, true, false, []Resource{boolDep2})
	}()

	a := All(stringOut, boolOut).ApplyT(func(args []interface{}) (string, error) {
		s := args[0].(string)
		b := args[1].(bool)
		return fmt.Sprintf("%s: %v", s, b), nil
	})

	v, known, secret, deps, err := await(a)
	assert.Equal(t, "hello: true", v)
	assert.True(t, known)
	assert.False(t, secret)
	assert.ElementsMatch(t, []Resource{stringDep1, stringDep2, boolDep1, boolDep2}, deps)
	assert.NoError(t, err)
}

func testMixedWaitGroups(t *testing.T, combine func(o1, o2 Output) Output) {
	var wg1, wg2 workGroup

	o1 := newOutput(&wg1, anyOutputType)
	o2 := newOutput(&wg2, anyOutputType)

	gate := make(chan chan bool)
	combine(o1, o2).ApplyT(func(_ interface{}) interface{} {
		<-gate <- true
		return 0
	})

	wg1Done, wg2Done := false, false
	go func() {
		wg1.Wait()
		wg1Done = true
		wg2.Wait()
		wg2Done = true
	}()

	o1.getState().resolve(0, true, true, nil)
	o2.getState().resolve(0, true, true, nil)

	c := make(chan bool)
	gate <- c
	assert.False(t, wg1Done)
	assert.False(t, wg2Done)
	<-c
	wg1.Wait()
	wg2.Wait()

}

func TestMixedWaitGroupsAll(t *testing.T) {
	testMixedWaitGroups(t, func(o1, o2 Output) Output {
		return All(o1, o2)
	})
}

func TestMixedWaitGroupsAny(t *testing.T) {
	testMixedWaitGroups(t, func(o1, o2 Output) Output {
		return Any(struct{ O1, O2 Output }{o1, o2})
	})
}

func TestMixedWaitGroupsApply(t *testing.T) {
	testMixedWaitGroups(t, func(o1, o2 Output) Output {
		return o1.ApplyT(func(_ interface{}) interface{} {
			return o2
		})
	})
}

type Foo interface {
}

type FooInput interface {
	Input

	ToFooOutput() Output
}
type FooArgs struct {
}

func (FooArgs) ElementType() reflect.Type {
	return nil
}

func TestRegisterInputType(t *testing.T) {
	assert.PanicsWithError(t, "expected string to be an interface", func() {
		RegisterInputType(reflect.TypeOf(""), FooArgs{})
	})
	assert.PanicsWithError(t, "expected pulumi.Foo to implement pulumi.Input", func() {
		RegisterInputType(reflect.TypeOf((*Foo)(nil)).Elem(), FooArgs{})
	})
	assert.PanicsWithError(t, "expected pulumi.FooArgs to implement interface pulumi.FooInput", func() {
		RegisterInputType(reflect.TypeOf((*FooInput)(nil)).Elem(), FooArgs{})
	})
}
