// Copyright 2016-2023, 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.

package deploy

import (
	"context"
	"testing"

	"github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest"
	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
	"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
	"github.com/stretchr/testify/assert"
)

func TestBuiltinProvider(t *testing.T) {
	t.Parallel()
	t.Run("Close", func(t *testing.T) {
		t.Parallel()
		p := &builtinProvider{}
		assert.NoError(t, p.Close())
	})
	t.Run("Pkg", func(t *testing.T) {
		t.Parallel()
		p := &builtinProvider{}
		assert.Equal(t, tokens.Package("pulumi"), p.Pkg())
	})
	t.Run("GetSchema", func(t *testing.T) {
		t.Parallel()
		p := &builtinProvider{}
		b, err := p.GetSchema(0)
		assert.NoError(t, err)
		assert.Equal(t, []byte("{}"), b)
	})
	t.Run("GetMapping", func(t *testing.T) {
		t.Parallel()
		p := &builtinProvider{}
		b, s, err := p.GetMapping("key", "provider")
		assert.NoError(t, err)
		assert.Nil(t, b)
		assert.Equal(t, "", s)
	})
	t.Run("GetMappings", func(t *testing.T) {
		t.Parallel()
		p := &builtinProvider{}
		strs, err := p.GetMappings("key")
		assert.NoError(t, err)
		assert.Empty(t, strs)
	})
	t.Run("Check", func(t *testing.T) {
		t.Parallel()
		t.Run("builtin only supports stack reference type", func(t *testing.T) {
			t.Parallel()
			p := &builtinProvider{}
			_, _, err := p.Check(
				resource.CreateURN("foo", "not-stack-reference-type", "", "proj", "stack"),
				resource.PropertyMap{}, resource.PropertyMap{}, true, nil)
			assert.ErrorContains(t, err, "unrecognized resource type")
		})
		t.Run("missing `name` input property", func(t *testing.T) {
			t.Parallel()
			p := &builtinProvider{
				diag: &deploytest.NoopSink{},
			}
			_, failures, err := p.Check(
				resource.CreateURN("foo", stackReferenceType, "", "proj", "stack"),
				resource.PropertyMap{}, resource.PropertyMap{}, true, nil)
			assert.Equal(t, []plugin.CheckFailure{
				{
					Property: "name",
					Reason:   `missing required property "name"`,
				},
			}, failures)
			assert.NoError(t, err)
		})
		t.Run(`property "name" must be a string`, func(t *testing.T) {
			t.Parallel()
			p := &builtinProvider{
				diag: &deploytest.NoopSink{},
			}
			_, failures, err := p.Check(
				resource.CreateURN("foo", stackReferenceType, "", "proj", "stack"),
				resource.PropertyMap{}, resource.PropertyMap{
					"name": resource.NewNumberProperty(10),
				}, true, nil)
			assert.Equal(t, []plugin.CheckFailure{
				{
					Property: "name",
					Reason:   `property "name" must be a string`,
				},
			}, failures)
			assert.NoError(t, err)
		})
		t.Run("ok", func(t *testing.T) {
			t.Parallel()
			p := &builtinProvider{
				diag: &deploytest.NoopSink{},
			}
			checked, failures, err := p.Check(
				resource.CreateURN("foo", stackReferenceType, "", "proj", "stack"),
				resource.PropertyMap{}, resource.PropertyMap{
					"name": resource.NewStringProperty("res-name"),
				}, true, nil)
			assert.Nil(t, failures)
			assert.NoError(t, err)
			assert.Equal(t, resource.PropertyMap{
				"name": resource.NewStringProperty("res-name"),
			}, checked)
		})
	})
	t.Run("Update (always fails)", func(t *testing.T) {
		t.Parallel()
		assert.Panics(t, func() {
			p := &builtinProvider{}

			oldOutputs := resource.PropertyMap{"cookie": resource.NewStringProperty("yum")}
			_, _, err := p.Update(
				resource.CreateURN("foo", "not-stack-reference-type", "", "proj", "stack"),
				"some-id",
				nil, oldOutputs,
				resource.PropertyMap{}, 0, nil, false,
			)
			contract.Ignore(err)
		})
	})
	t.Run("Construct (always fails)", func(t *testing.T) {
		t.Parallel()
		p := &builtinProvider{}
		_, err := p.Construct(plugin.ConstructInfo{}, "", "", "", resource.PropertyMap{}, plugin.ConstructOptions{})
		assert.ErrorContains(t, err, "builtin resources may not be constructed")
	})
	t.Run("Invoke", func(t *testing.T) {
		t.Parallel()
		t.Run(readStackOutputs, func(t *testing.T) {
			t.Parallel()
			t.Run("err", func(t *testing.T) {
				t.Parallel()
				p := &builtinProvider{}
				_, _, err := p.Invoke(readStackOutputs, resource.PropertyMap{
					"name": resource.NewStringProperty("res-name"),
				})
				assert.ErrorContains(t, err, "no backend client is available")
			})
			t.Run("ok", func(t *testing.T) {
				t.Parallel()
				var called bool
				p := &builtinProvider{
					backendClient: &deploytest.BackendClient{
						GetStackOutputsF: func(ctx context.Context, name string) (resource.PropertyMap, error) {
							called = true
							return resource.PropertyMap{
								"normal": resource.NewStringProperty("foo"),
								"secret": resource.MakeSecret(resource.NewStringProperty("bar")),
							}, nil
						},
					},
				}
				out, failures, err := p.Invoke(readStackOutputs, resource.PropertyMap{
					"name": resource.NewStringProperty("res-name"),
				})
				assert.NoError(t, err)
				assert.True(t, called)
				assert.Nil(t, failures)

				assert.Equal(t, "res-name", out["name"].V)

				assert.Equal(t, "foo", out["outputs"].ObjectValue()["normal"].StringValue())
				assert.Len(t, out["secretOutputNames"].V, 1)
			})
		})
		t.Run(readStackResourceOutputs, func(t *testing.T) {
			t.Parallel()
			t.Run("err", func(t *testing.T) {
				t.Parallel()
				p := &builtinProvider{}
				_, _, err := p.Invoke(readStackResourceOutputs, resource.PropertyMap{
					"stackName": resource.NewStringProperty("res-name"),
				})
				assert.ErrorContains(t, err, "no backend client is available")
			})
			t.Run("ok", func(t *testing.T) {
				t.Parallel()
				var called bool
				p := &builtinProvider{
					backendClient: &deploytest.BackendClient{
						GetStackResourceOutputsF: func(ctx context.Context, name string) (resource.PropertyMap, error) {
							called = true
							return resource.PropertyMap{}, nil
						},
					},
				}
				_, _, err := p.Invoke(readStackResourceOutputs, resource.PropertyMap{
					"stackName": resource.NewStringProperty("res-name"),
				})
				assert.NoError(t, err)
				assert.True(t, called)
			})
		})
		t.Run(getResource, func(t *testing.T) {
			t.Parallel()
			t.Run("err", func(t *testing.T) {
				t.Parallel()
				p := &builtinProvider{
					resources: &resourceMap{},
				}
				_, _, err := p.Invoke(getResource, resource.PropertyMap{
					"urn": resource.NewStringProperty("res-name"),
				})
				assert.ErrorContains(t, err, "unknown resource")
			})
		})
	})
	t.Run("StreamInvoke (unimplemented)", func(t *testing.T) {
		t.Parallel()
		p := &builtinProvider{}
		_, err := p.StreamInvoke(tokens.ModuleMember(""), resource.PropertyMap{}, nil)
		assert.ErrorContains(t, err, "the builtin provider does not implement streaming invokes")
	})
	t.Run("Call (unimplemented)", func(t *testing.T) {
		t.Parallel()
		p := &builtinProvider{}
		_, err := p.Call(tokens.ModuleMember(""), resource.PropertyMap{},
			plugin.CallInfo{}, plugin.CallOptions{})
		assert.ErrorContains(t, err, "the builtin provider does not implement call")
	})
	t.Run("GetPluginInfo (always fails)", func(t *testing.T) {
		t.Parallel()
		p := &builtinProvider{}
		_, err := p.GetPluginInfo()
		assert.ErrorContains(t, err, "the builtin provider does not report plugin info")
	})
	t.Run("SignalCancellation", func(t *testing.T) {
		t.Parallel()
		var called bool
		p := &builtinProvider{
			cancel: func() {
				called = true
			},
		}
		assert.NoError(t, p.SignalCancellation())
		assert.True(t, called)
		// Ensure idempotent.
		assert.NoError(t, p.SignalCancellation())
	})
}