package terraform

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

	"github.com/databricks/cli/bundle"
	"github.com/databricks/cli/bundle/config"
	mock "github.com/databricks/cli/internal/mocks/libs/filer"
	"github.com/databricks/cli/libs/filer"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"
)

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

	ctrl := gomock.NewController(t)
	mock := mock.NewMockFiler(ctrl)
	mock.
		EXPECT().
		Read(gomock.Any(), gomock.Eq(TerraformStateFileName)).
		Return(io.NopCloser(bytes.NewReader(buf)), merr).
		Times(1)
	return mock
}

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

func TestStatePullLocalMissingRemoteMissing(t *testing.T) {
	m := &statePull{
		identityFiler(mockStateFilerForPull(t, nil, os.ErrNotExist)),
	}

	ctx := context.Background()
	b := statePullTestBundle(t)
	err := bundle.Apply(ctx, b, m)
	assert.NoError(t, err)

	// Confirm that no local state file has been written.
	_, err = os.Stat(localStateFile(t, ctx, b))
	assert.ErrorIs(t, err, fs.ErrNotExist)
}

func TestStatePullLocalMissingRemotePresent(t *testing.T) {
	m := &statePull{
		identityFiler(mockStateFilerForPull(t, map[string]int{"serial": 5}, nil)),
	}

	ctx := context.Background()
	b := statePullTestBundle(t)
	err := bundle.Apply(ctx, b, m)
	assert.NoError(t, err)

	// Confirm that the local state file has been updated.
	localState := readLocalState(t, ctx, b)
	assert.Equal(t, map[string]int{"serial": 5}, localState)
}

func TestStatePullLocalStale(t *testing.T) {
	m := &statePull{
		identityFiler(mockStateFilerForPull(t, map[string]int{"serial": 5}, nil)),
	}

	ctx := context.Background()
	b := statePullTestBundle(t)

	// Write a stale local state file.
	writeLocalState(t, ctx, b, map[string]int{"serial": 4})
	err := bundle.Apply(ctx, b, m)
	assert.NoError(t, err)

	// Confirm that the local state file has been updated.
	localState := readLocalState(t, ctx, b)
	assert.Equal(t, map[string]int{"serial": 5}, localState)
}

func TestStatePullLocalEqual(t *testing.T) {
	m := &statePull{
		identityFiler(mockStateFilerForPull(t, map[string]int{"serial": 5, "some_other_key": 123}, nil)),
	}

	ctx := context.Background()
	b := statePullTestBundle(t)

	// Write a local state file with the same serial as the remote.
	writeLocalState(t, ctx, b, map[string]int{"serial": 5})
	err := bundle.Apply(ctx, b, m)
	assert.NoError(t, err)

	// Confirm that the local state file has not been updated.
	localState := readLocalState(t, ctx, b)
	assert.Equal(t, map[string]int{"serial": 5}, localState)
}

func TestStatePullLocalNewer(t *testing.T) {
	m := &statePull{
		identityFiler(mockStateFilerForPull(t, map[string]int{"serial": 5, "some_other_key": 123}, nil)),
	}

	ctx := context.Background()
	b := statePullTestBundle(t)

	// Write a local state file with a newer serial as the remote.
	writeLocalState(t, ctx, b, map[string]int{"serial": 6})
	err := bundle.Apply(ctx, b, m)
	assert.NoError(t, err)

	// Confirm that the local state file has not been updated.
	localState := readLocalState(t, ctx, b)
	assert.Equal(t, map[string]int{"serial": 6}, localState)
}