// 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 diy

import (
	"context"
	"path/filepath"
	"testing"

	"github.com/pulumi/pulumi/sdk/v3/go/common/env"
	"github.com/pulumi/pulumi/sdk/v3/go/common/testing/diagtest"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gocloud.dev/blob/fileblob"
	"gocloud.dev/blob/memblob"
)

func TestEnsurePulumiMeta(t *testing.T) {
	t.Parallel()

	mkmap := func(value string) env.MapStore {
		m := make(env.MapStore)
		m[env.DIYBackendLegacyLayout.Var().Name()] = value
		return m
	}

	tests := []struct {
		desc string
		give map[string]string // files in the bucket
		env  env.MapStore      // environment variables
		want pulumiMeta
	}{
		{
			// Empty bucket should be initialized to
			// the current version by default.
			desc: "empty",
			want: pulumiMeta{Version: 1},
		},
		{
			// Use legacy mode even for the new bucket
			// because the environment variable is "1".
			desc: "empty/legacy",
			env:  mkmap("1"),
			want: pulumiMeta{Version: 0},
		},
		{
			// Use legacy mode even for the new bucket
			// because the environment variable is "true".
			desc: "empty/legacy/true",
			env:  mkmap("true"),
			want: pulumiMeta{Version: 0},
		},
		{
			// Legacy mode is disabled by setting the env var
			// to "false".
			// This is also the default behavior.
			desc: "empty/legacy/false",
			env:  mkmap("false"),
			want: pulumiMeta{Version: 1},
		},
		{
			// Non-empty bucket without a version file
			// should get version 0 for legacy mode.
			desc: "legacy",
			give: map[string]string{
				".pulumi/stacks/a.json": `{}`,
			},
			want: pulumiMeta{Version: 0},
		},
		{
			desc: "version 0",
			give: map[string]string{
				".pulumi/meta.yaml": `version: 0`,
			},
			want: pulumiMeta{Version: 0},
		},
		{
			// Non-empty bucket with a version file
			// should get whatever is in the file.
			desc: "version 1",
			give: map[string]string{
				".pulumi/meta.yaml": `version: 1`,
			},
			want: pulumiMeta{Version: 1},
		},
		{
			desc: "future version",
			give: map[string]string{
				".pulumi/meta.yaml": `version: 42`,
			},
			want: pulumiMeta{Version: 42},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.desc, func(t *testing.T) {
			t.Parallel()

			b := memblob.OpenBucket(nil)
			ctx := context.Background()
			for name, body := range tt.give {
				require.NoError(t, b.WriteAll(ctx, name, []byte(body), nil))
			}

			state, err := ensurePulumiMeta(ctx, b, env.NewEnv(tt.env))
			require.NoError(t, err)
			assert.Equal(t, &tt.want, state)
		})
	}
}

func TestEnsurePulumiMeta_corruption(t *testing.T) {
	t.Parallel()

	tests := []struct {
		desc    string
		give    string // contents of meta.yaml
		wantErr string
	}{
		{
			desc:    "empty",
			give:    ``,
			wantErr: `corrupt store: missing version in ".pulumi/meta.yaml"`,
		},
		{
			desc:    "other fields",
			give:    `foo: bar`,
			wantErr: `corrupt store: missing version in ".pulumi/meta.yaml"`,
		},
		{
			desc:    "corrupt version",
			give:    `version: foo`,
			wantErr: `corrupt store: unmarshal ".pulumi/meta.yaml"`,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.desc, func(t *testing.T) {
			t.Parallel()

			b := memblob.OpenBucket(nil)
			ctx := context.Background()
			require.NoError(t, b.WriteAll(ctx, ".pulumi/meta.yaml", []byte(tt.give), nil))

			_, err := ensurePulumiMeta(context.Background(), b, env.NewEnv(nil))
			assert.ErrorContains(t, err, tt.wantErr)
		})
	}
}

// Writes a metadata file to the bucket and reads it back.
func TestMeta_roundTrip(t *testing.T) {
	t.Parallel()

	tests := []struct {
		desc string
		give pulumiMeta
	}{
		{desc: "zero", give: pulumiMeta{Version: 0}},
		{desc: "one", give: pulumiMeta{Version: 1}},
		{desc: "future", give: pulumiMeta{Version: 42}},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.desc, func(t *testing.T) {
			t.Parallel()

			b := memblob.OpenBucket(nil)
			// The bucket is always non-empty,
			// so we won't automatically try to use version 1.
			require.NoError(t,
				b.WriteAll(context.Background(), ".pulumi/stacks/dev.json", []byte("bar"), nil))

			ctx := context.Background()
			require.NoError(t, tt.give.WriteTo(ctx, b))

			got, err := ensurePulumiMeta(ctx, b, env.NewEnv(nil))
			require.NoError(t, err)
			assert.Equal(t, &tt.give, got)
		})
	}
}

// Verifies that we don't write a metadata file with version 0.
func TestMeta_WriteTo_zero(t *testing.T) {
	t.Parallel()

	tmpDir := t.TempDir()
	bucket, err := fileblob.OpenBucket(tmpDir, nil)
	require.NoError(t, err)

	ctx := context.Background()
	require.NoError(t, (&pulumiMeta{
		Version: 0,
	}).WriteTo(ctx, bucket))

	assert.NoFileExists(t, filepath.Join(tmpDir, ".pulumi", "meta.yaml"))
}

// Verify that we don't create a metadata file with version 0 in buckets
// that have other files.
func TestNew_noMetaOnInit(t *testing.T) {
	t.Parallel()

	tmpDir := t.TempDir()
	bucket, err := fileblob.OpenBucket(tmpDir, nil)
	require.NoError(t, err)
	require.NoError(t,
		bucket.WriteAll(context.Background(), ".pulumi/stacks/dev.json", []byte("bar"), nil))

	ctx := context.Background()
	_, err = New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
	require.NoError(t, err)

	assert.NoFileExists(t, filepath.Join(tmpDir, ".pulumi", "meta.yaml"))
}