// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package clistate

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"strings"
	"testing"

	"github.com/opentofu/opentofu/internal/configs/configschema"
	"github.com/zclconf/go-cty/cty"
)

func TestReadState_EmptyFile(t *testing.T) {
	reader := bytes.NewReader([]byte{})
	_, err := ReadState(reader)
	if !errors.Is(err, ErrNoState) {
		t.Fatalf("expected ErrNoState, got %T", err)
	}
}

func TestReadState_NilFile(t *testing.T) {
	var f *os.File
	_, err := ReadState(f)
	if !errors.Is(err, ErrNoState) {
		t.Fatalf("expected ErrNoState, got %T", err)
	}
}

func TestReadState_ValidState(t *testing.T) {
	state := &CLIState{
		Version: StateVersion,
	}

	buf := &bytes.Buffer{}
	if err := WriteState(state, buf); err != nil {
		t.Fatalf("failed to write state: %v", err)
	}

	result, err := ReadState(buf)
	if err != nil {
		t.Fatalf("ReadState failed: %v", err)
	}

	if result.Version != StateVersion {
		t.Errorf("expected version %d, got %d", StateVersion, result.Version)
	}
}

func TestReadState_InvalidJSON(t *testing.T) {
	reader := strings.NewReader("{invalid json")
	_, err := ReadState(reader)
	if err == nil {
		t.Fatal("expected error for invalid JSON, got nil")
	}
	if !strings.Contains(err.Error(), "decoding CLI state file version failed") {
		t.Errorf("unexpected error message: %v", err)
	}
}

func TestReadState_MissingVersion(t *testing.T) {
	reader := strings.NewReader(`{}`)
	_, err := ReadState(reader)
	if err == nil {
		t.Fatal("expected error for missing version, got nil")
	}
	if !strings.Contains(err.Error(), "does not support CLI state version 0") {
		t.Errorf("unexpected error message: %v", err)
	}
}

func TestReadState_UnsupportedVersion(t *testing.T) {
	tests := []struct {
		name    string
		version int
	}{
		{"state file v1", 1},
		{"state file v2", 2},
		{"state file v4", 4},
		{"future version", 10},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			input := fmt.Sprintf(`{"version": %d}`, tt.version)
			_, err := ReadState(strings.NewReader(input))
			if err == nil {
				t.Fatalf("expected error for version %d, got nil", tt.version)
			}
			want := fmt.Sprintf("does not support CLI state version %d", tt.version)
			if !strings.Contains(err.Error(), want) {
				t.Errorf("got error %q, want substring %q", err.Error(), want)
			}
		})
	}
}

func TestWriteState_NilState(t *testing.T) {
	buf := &bytes.Buffer{}
	err := WriteState(nil, buf)
	if err != nil {
		t.Fatalf("WriteState with nil should not error, got: %v", err)
	}
	if buf.Len() != 0 {
		t.Errorf("expected empty buffer for nil state, got %d bytes", buf.Len())
	}
}

func TestWriteState_ValidState(t *testing.T) {
	state := &CLIState{
		Version: StateVersion,
	}

	buf := &bytes.Buffer{}
	err := WriteState(state, buf)
	if err != nil {
		t.Fatalf("WriteState failed: %v", err)
	}

	var parsed CLIState
	if err := json.Unmarshal(buf.Bytes(), &parsed); err != nil {
		t.Fatalf("failed to parse written state: %v", err)
	}

	if parsed.Version != StateVersion {
		t.Errorf("expected version %d, got %d", StateVersion, parsed.Version)
	}
}

func TestWriteState_SetsVersion(t *testing.T) {
	state := &CLIState{
		Version: 99, // Invalid version
	}

	buf := &bytes.Buffer{}
	err := WriteState(state, buf)
	if err != nil {
		t.Fatalf("WriteState failed: %v", err)
	}

	var parsed CLIState
	if err := json.Unmarshal(buf.Bytes(), &parsed); err != nil {
		t.Fatalf("failed to parse written state: %v", err)
	}

	if parsed.Version != StateVersion {
		t.Errorf("expected version to be corrected to %d, got %d", StateVersion, parsed.Version)
	}
}

func TestNewState(t *testing.T) {
	state := NewState()
	if state.Version != StateVersion {
		t.Errorf("expected version %d, got %d", StateVersion, state.Version)
	}
}

func TestCLIState_DeepCopy(t *testing.T) {
	original := &CLIState{
		Version: StateVersion,
	}

	copied := original.DeepCopy()
	if copied.Version != original.Version {
		t.Errorf("version mismatch: %d != %d", copied.Version, original.Version)
	}
}

func TestCLIState_DeepCopy_Nil(t *testing.T) {
	var state *CLIState
	copied := state.DeepCopy()
	if copied != nil {
		t.Errorf("expected nil for DeepCopy of nil, got %T", copied)
	}
}

func TestBackendState_Empty(t *testing.T) {
	tests := []struct {
		name     string
		backend  *BackendState
		expected bool
	}{
		{"nil backend", nil, true},
		{"empty type", &BackendState{Type: ""}, true},
		{"with type", &BackendState{Type: "local"}, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := tt.backend.Empty(); got != tt.expected {
				t.Errorf("Empty() = %v, want %v", got, tt.expected)
			}
		})
	}
}

func TestBackendState_ConfigSetAndGet(t *testing.T) {
	schema := &configschema.Block{
		Attributes: map[string]*configschema.Attribute{
			"path": {
				Type:     cty.String,
				Optional: true,
			},
		},
	}

	backend := &BackendState{Type: "local"}

	val := cty.ObjectVal(map[string]cty.Value{
		"path": cty.StringVal("/tmp/state"),
	})

	err := backend.SetConfig(val, schema)
	if err != nil {
		t.Fatalf("SetConfig failed: %v", err)
	}

	retrieved, err := backend.Config(schema)
	if err != nil {
		t.Fatalf("Config failed: %v", err)
	}

	if !val.RawEquals(retrieved) {
		t.Errorf("retrieved config doesn't match original:\nwant: %#v\ngot:  %#v", val, retrieved)
	}
}

func TestBackendState_ConfigNil(t *testing.T) {
	schema := &configschema.Block{
		Attributes: map[string]*configschema.Attribute{
			"path": {Type: cty.String, Optional: true},
		},
	}

	var backend *BackendState
	val, err := backend.Config(schema)
	if err != nil {
		t.Fatalf("Config on nil backend failed: %v", err)
	}

	expectedType := schema.ImpliedType()
	if !val.Type().Equals(expectedType) {
		t.Errorf("expected type %s for nil backend, got %s", expectedType.FriendlyName(), val.Type().FriendlyName())
	}
	if !val.IsNull() {
		t.Error("expected null value for nil backend")
	}
}

func TestBackendState_ForPlan(t *testing.T) {
	schema := &configschema.Block{
		Attributes: map[string]*configschema.Attribute{
			"path": {
				Type:     cty.String,
				Optional: true,
			},
		},
	}

	backend := &BackendState{
		Type: "local",
		Hash: 12345,
	}

	val := cty.ObjectVal(map[string]cty.Value{
		"path": cty.StringVal("/tmp/state"),
	})

	err := backend.SetConfig(val, schema)
	if err != nil {
		t.Fatalf("SetConfig failed: %v", err)
	}

	plan, err := backend.ForPlan(schema, "default")
	if err != nil {
		t.Fatalf("ForPlan failed: %v", err)
	}

	if plan == nil {
		t.Fatal("expected non-nil plan backend")
	}
}

func TestBackendState_ForPlanNil(t *testing.T) {
	schema := &configschema.Block{
		Attributes: map[string]*configschema.Attribute{
			"path": {Type: cty.String, Optional: true},
		},
	}

	var backend *BackendState
	plan, err := backend.ForPlan(schema, "default")
	if err != nil {
		t.Fatalf("ForPlan on nil backend failed: %v", err)
	}

	if plan != nil {
		t.Errorf("expected nil plan for nil backend, got %v", plan)
	}
}

func TestReadWriteRoundTrip(t *testing.T) {
	original := &CLIState{
		Version: StateVersion,
		Backend: &BackendState{
			Type: "s3",
			Hash: 12345,
		},
	}

	buf := &bytes.Buffer{}
	if err := WriteState(original, buf); err != nil {
		t.Fatalf("WriteState failed: %v", err)
	}

	result, err := ReadState(buf)
	if err != nil {
		t.Fatalf("ReadState failed: %v", err)
	}

	if result.Version != StateVersion {
		t.Errorf("version mismatch: got %d, want %d", result.Version, StateVersion)
	}

	if result.Backend == nil {
		t.Fatal("expected backend to be non-nil")
	}

	if result.Backend.Type != "s3" {
		t.Errorf("backend type: got %q, want %q", result.Backend.Type, "s3")
	}
}
