package packs

import (
	"context"
	"encoding/json"
	"reflect"
	"testing"
	"unsafe"
)

type projectionOwnershipMutatingMarshaler struct {
	Values []string `json:"values"`
}

func (value projectionOwnershipMutatingMarshaler) MarshalJSON() ([]byte, error) {
	value.Values[0] = "marshaled"
	type projectionWire struct {
		Values []string `json:"values"`
	}
	return json.Marshal(projectionWire{Values: value.Values})
}

func TestSourceBackedManagedComponentProjectionReadsAreDetached(t *testing.T) {
	owned := projectionOwnershipFixture()
	appliedOwnedState := false
	component := &sourceBackedManagedComponent{
		packName: "test-pack",
		manifestComponent: ManifestComponent{
			Kind: ComponentKindListFields, BusinessKey: "Widget", Manageability: ManageabilityLocked,
		},
		expectedProjection: func() (any, error) { return owned, nil },
		liveProjection:     func(context.Context, VerifyDeps) (any, error) { return owned, nil },
		apply: func(context.Context, ApplyDeps) error {
			appliedOwnedState = reflect.DeepEqual(owned, projectionOwnershipFixture())
			return nil
		},
	}
	pack := &SourceBackedPack{components: []PackComponent{component}}

	expected, err := pack.Components()[0].ExpectedProjection()
	if err != nil {
		t.Fatalf("ExpectedProjection() error = %v", err)
	}
	mutateProjectionOwnershipFixture(t, expected)
	assertProjectionOwnershipFixture(t, pack.Components()[0], false)

	live, err := pack.Components()[0].LiveProjection(context.Background(), VerifyDeps{})
	if err != nil {
		t.Fatalf("LiveProjection() error = %v", err)
	}
	mutateProjectionOwnershipFixture(t, live)
	assertProjectionOwnershipFixture(t, pack.Components()[0], true)

	plan, err := NewInstallPlan(pack.Components())
	if err != nil {
		t.Fatalf("NewInstallPlan() error = %v", err)
	}
	phases := plan.Phases()
	if len(phases) != 1 {
		t.Fatalf("plan phases = %v, want one", phases)
	}
	planned := plan.ComponentsForPhase(phases[0])
	projection, err := planned[0].ExpectedProjection()
	if err != nil {
		t.Fatalf("planned ExpectedProjection() error = %v", err)
	}
	mutateProjectionOwnershipFixture(t, projection)
	assertProjectionOwnershipFixture(t, plan.ComponentsForPhase(phases[0])[0], false)
	if err := plan.Apply(context.Background(), ApplyDeps{}, InstallPlanHooks{}); err != nil {
		t.Fatalf("plan.Apply() error = %v", err)
	}
	if !appliedOwnedState {
		t.Fatal("mutating an exposed projection changed later Apply state")
	}
}

func TestClonePackComponentProjectionRejectsHiddenMutableValues(t *testing.T) {
	type hiddenValue struct {
		Value any `json:"-"`
	}
	value := 1
	for _, testCase := range []struct {
		name  string
		value any
	}{
		{name: "function", value: func() {}},
		{name: "channel", value: make(chan struct{})},
		{name: "uintptr", value: uintptr(1)},
		{name: "unsafe pointer", value: unsafe.Pointer(&value)},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			if _, err := clonePackComponentProjection(hiddenValue{Value: testCase.value}); err == nil {
				t.Fatal("hidden mutable projection value was accepted")
			}
		})
	}

	type privateAlias struct {
		values []string
	}
	if _, err := clonePackComponentProjection(privateAlias{values: []string{"before"}}); err == nil {
		t.Fatal("mutable unexported projection field was accepted")
	}

	type cyclicProjection struct {
		Next *cyclicProjection `json:"-"`
	}
	cycle := &cyclicProjection{}
	cycle.Next = cycle
	if _, err := clonePackComponentProjection(cycle); err == nil {
		t.Fatal("hidden projection cycle was accepted")
	}
}

func TestClonePackComponentProjectionValidationCannotMutateSourceOrResult(t *testing.T) {
	owned := projectionOwnershipMutatingMarshaler{Values: []string{"before"}}
	cloned, err := clonePackComponentProjection(owned)
	if err != nil {
		t.Fatalf("clonePackComponentProjection() error = %v", err)
	}
	if owned.Values[0] != "before" {
		t.Fatalf("owned value = %q, want before", owned.Values[0])
	}
	if got := cloned.(projectionOwnershipMutatingMarshaler).Values[0]; got != "before" {
		t.Fatalf("cloned value = %q, want before", got)
	}
}

func projectionOwnershipFixture() map[string]any {
	pointerValue := "before"
	return map[string]any{
		"nested": []any{map[string]any{
			"number": json.Number("9007199254740993"),
			"ratio":  float64(1.5),
			"raw":    json.RawMessage(`{"before":true}`),
		}},
		"empty":    map[string]any{},
		"nilSlice": []string(nil),
		"pointer":  &pointerValue,
	}
}

func mutateProjectionOwnershipFixture(t *testing.T, value any) {
	t.Helper()
	projection := value.(map[string]any)
	nested := projection["nested"].([]any)[0].(map[string]any)
	nested["number"] = json.Number("1")
	nested["ratio"] = float64(2.5)
	nested["raw"].(json.RawMessage)[0] = '!'
	projection["empty"].(map[string]any)["mutated"] = true
	*projection["pointer"].(*string) = "mutated"
}

func assertProjectionOwnershipFixture(t *testing.T, component PackComponent, live bool) {
	t.Helper()
	var (
		projection any
		err        error
	)
	if live {
		projection, err = component.LiveProjection(context.Background(), VerifyDeps{})
	} else {
		projection, err = component.ExpectedProjection()
	}
	if err != nil {
		t.Fatalf("projection read error = %v", err)
	}
	if !reflect.DeepEqual(projection, projectionOwnershipFixture()) {
		t.Fatalf("projection = %#v, want detached fixture", projection)
	}
	nested := projection.(map[string]any)["nested"].([]any)[0].(map[string]any)
	if _, ok := nested["number"].(json.Number); !ok {
		t.Fatalf("number type = %T, want json.Number", nested["number"])
	}
	if _, ok := nested["ratio"].(float64); !ok {
		t.Fatalf("ratio type = %T, want float64", nested["ratio"])
	}
	if projection.(map[string]any)["empty"].(map[string]any) == nil ||
		projection.(map[string]any)["nilSlice"].([]string) != nil {
		t.Fatal("nil/non-nil-empty projection semantics changed")
	}
}
