package terraform

import (
	"bytes"
	"context"
	"encoding/json"
	"io"
	"io/fs"
	"os"
	"testing"

	"github.com/databricks/cli/bundle"
	"github.com/databricks/cli/bundle/config"
	mockfiler "github.com/databricks/cli/internal/mocks/libs/filer"
	"github.com/databricks/cli/libs/filer"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

func mockStateFilerForPull(t *testing.T, contents map[string]any, merr error) filer.Filer {
	buf, err := json.Marshal(contents)
	assert.NoError(t, err)

	f := mockfiler.NewMockFiler(t)
	f.
		EXPECT().
		Read(mock.Anything, TerraformStateFileName).
		Return(io.NopCloser(bytes.NewReader(buf)), merr).
		Times(1)
	return f
}

func statePullTestBundle(t *testing.T) *bundle.Bundle {
	return &bundle.Bundle{
		BundleRootPath: t.TempDir(),
		Config: config.Root{
			Bundle: config.Bundle{
				Target: "default",
			},
		},
	}
}

func TestStatePullLocalErrorWhenRemoteHasNoLineage(t *testing.T) {
	m := &statePull{}

	t.Run("no local state", func(t *testing.T) {
		// setup remote state.
		m.filerFactory = identityFiler(mockStateFilerForPull(t, map[string]any{"serial": 5}, nil))

		ctx := context.Background()
		b := statePullTestBundle(t)
		diags := bundle.Apply(ctx, b, m)
		assert.EqualError(t, diags.Error(), "remote state file does not have a lineage")
	})

	t.Run("local state with lineage", func(t *testing.T) {
		// setup remote state.
		m.filerFactory = identityFiler(mockStateFilerForPull(t, map[string]any{"serial": 5}, nil))

		ctx := context.Background()
		b := statePullTestBundle(t)
		writeLocalState(t, ctx, b, map[string]any{"serial": 5, "lineage": "aaaa"})

		diags := bundle.Apply(ctx, b, m)
		assert.EqualError(t, diags.Error(), "remote state file does not have a lineage")
	})
}

func TestStatePullLocal(t *testing.T) {
	tcases := []struct {
		name string

		// remote state before applying the pull mutators
		remote map[string]any

		// local state before applying the pull mutators
		local map[string]any

		// expected local state after applying the pull mutators
		expected map[string]any
	}{
		{
			name:     "remote missing, local missing",
			remote:   nil,
			local:    nil,
			expected: nil,
		},
		{
			name:   "remote missing, local present",
			remote: nil,
			local:  map[string]any{"serial": 5, "lineage": "aaaa"},
			// fallback to local state, since remote state is missing.
			expected: map[string]any{"serial": float64(5), "lineage": "aaaa"},
		},
		{
			name:   "local stale",
			remote: map[string]any{"serial": 10, "lineage": "aaaa", "some_other_key": 123},
			local:  map[string]any{"serial": 5, "lineage": "aaaa"},
			// use remote, since remote is newer.
			expected: map[string]any{"serial": float64(10), "lineage": "aaaa", "some_other_key": float64(123)},
		},
		{
			name:   "local equal",
			remote: map[string]any{"serial": 5, "lineage": "aaaa", "some_other_key": 123},
			local:  map[string]any{"serial": 5, "lineage": "aaaa"},
			// use local state, since they are equal in terms of serial sequence.
			expected: map[string]any{"serial": float64(5), "lineage": "aaaa"},
		},
		{
			name:   "local newer",
			remote: map[string]any{"serial": 5, "lineage": "aaaa", "some_other_key": 123},
			local:  map[string]any{"serial": 6, "lineage": "aaaa"},
			// use local state, since local is newer.
			expected: map[string]any{"serial": float64(6), "lineage": "aaaa"},
		},
		{
			name:   "remote and local have different lineages",
			remote: map[string]any{"serial": 5, "lineage": "aaaa"},
			local:  map[string]any{"serial": 10, "lineage": "bbbb"},
			// use remote, since lineages do not match.
			expected: map[string]any{"serial": float64(5), "lineage": "aaaa"},
		},
		{
			name:   "local is missing lineage",
			remote: map[string]any{"serial": 5, "lineage": "aaaa"},
			local:  map[string]any{"serial": 10},
			// use remote, since local does not have lineage.
			expected: map[string]any{"serial": float64(5), "lineage": "aaaa"},
		},
	}

	for _, tc := range tcases {
		t.Run(tc.name, func(t *testing.T) {
			m := &statePull{}
			if tc.remote == nil {
				// nil represents no remote state file.
				m.filerFactory = identityFiler(mockStateFilerForPull(t, nil, os.ErrNotExist))
			} else {
				m.filerFactory = identityFiler(mockStateFilerForPull(t, tc.remote, nil))
			}

			ctx := context.Background()
			b := statePullTestBundle(t)
			if tc.local != nil {
				writeLocalState(t, ctx, b, tc.local)
			}

			diags := bundle.Apply(ctx, b, m)
			assert.NoError(t, diags.Error())

			if tc.expected == nil {
				// nil represents no local state file is expected.
				_, err := os.Stat(localStateFile(t, ctx, b))
				assert.ErrorIs(t, err, fs.ErrNotExist)
			} else {
				localState := readLocalState(t, ctx, b)
				assert.Equal(t, tc.expected, localState)

			}
		})
	}
}
