// © Broadcom. All Rights Reserved.
// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
// SPDX-License-Identifier: Apache-2.0

package simulator

import (
	"context"
	"fmt"
	"reflect"
	"sync"
	"testing"

	"github.com/vmware/govmomi"
	"github.com/vmware/govmomi/find"
	"github.com/vmware/govmomi/property"
	"github.com/vmware/govmomi/vim25/mo"
	"github.com/vmware/govmomi/vim25/types"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestPropertyDiff_SimpleFields(t *testing.T) {
	// Create a simple folder object
	folder := &mo.Folder{
		ManagedEntity: mo.ManagedEntity{
			Name: "original-name",
		},
	}
	folder.Self = types.ManagedObjectReference{Type: "Folder", Value: "folder-1"}

	// Create checkpoint
	checkpoint := Checkpoint(folder)

	// Modify the folder
	folder.Name = "new-name"

	// Get the diff
	changes := PropertyDiff(checkpoint, folder)

	// Verify we got exactly one change
	require.Len(t, changes, 1, "expected 1 change, got %d: %+v", len(changes), changes)

	// Verify the change details
	change := changes[0]
	assert.Equal(t, "name", change.Name, "expected change name 'name', got %q", change.Name)
	assert.Equal(t, types.PropertyChangeOpAssign, change.Op, "expected Op Assign, got %v", change.Op)
	assert.Equal(t, "new-name", change.Val, "expected Val 'new-name', got %v", change.Val)
}

func TestPropertyDiff_NestedFields(t *testing.T) {
	// Create a VM with guest info
	vm := &mo.VirtualMachine{
		Guest: &types.GuestInfo{
			IpAddress: "192.168.1.100",
			HostName:  "test-host",
		},
		Summary: types.VirtualMachineSummary{
			Guest: &types.VirtualMachineGuestSummary{
				IpAddress: "192.168.1.100",
				HostName:  "test-host",
			},
		},
	}
	vm.Self = types.ManagedObjectReference{Type: "VirtualMachine", Value: "vm-1"}
	vm.Name = "test-vm"

	// Create checkpoint
	checkpoint := Checkpoint(vm)

	// Modify nested fields
	vm.Guest.IpAddress = "10.0.0.50"
	vm.Summary.Guest.IpAddress = "10.0.0.50"

	// Get the diff
	changes := PropertyDiff(checkpoint, vm)

	// We should have changes for guest and summary.guest
	require.Len(t, changes, 2, "expected at least 2 changes, got %d: %+v", len(changes), changes)

	// Check that we have the expected property paths
	foundGuest := false
	foundSummaryGuest := false
	for _, c := range changes {
		if c.Name == "guest" {
			foundGuest = true
		}
		if c.Name == "summary" {
			foundSummaryGuest = true
		}
	}

	assert.True(t, foundGuest, "expected change for 'guest' property")
	assert.True(t, foundSummaryGuest, "expected change for 'summary' property")
}

func TestPropertyDiff_AddRemove(t *testing.T) {
	// Test Add operation (nil -> value)
	vm := &mo.VirtualMachine{}
	vm.Self = types.ManagedObjectReference{Type: "VirtualMachine", Value: "vm-1"}

	checkpoint := Checkpoint(vm)

	// Add a guest info
	vm.Guest = &types.GuestInfo{
		IpAddress: "192.168.1.100",
	}

	changes := PropertyDiff(checkpoint, vm)

	foundGuestAdd := false
	for _, c := range changes {
		if c.Name == "guest" && c.Op == types.PropertyChangeOpAdd {
			foundGuestAdd = true
		}
	}
	assert.True(t, foundGuestAdd, "expected Add operation for 'guest' property")

	// Test Remove operation (value -> nil)
	checkpoint2 := Checkpoint(vm)
	vm.Guest = nil

	changes2 := PropertyDiff(checkpoint2, vm)

	for _, c := range changes2 {
		if c.Name == "guest" && c.Op == types.PropertyChangeOpRemove {
			return
		}
	}
	t.Error("expected Remove operation for 'guest' property")
}

func TestPropertyDiff_NoChanges(t *testing.T) {
	folder := &mo.Folder{
		ManagedEntity: mo.ManagedEntity{
			Name: "test-folder",
		},
	}
	folder.Self = types.ManagedObjectReference{Type: "Folder", Value: "folder-1"}

	checkpoint := Checkpoint(folder)

	// No modifications
	changes := PropertyDiff(checkpoint, folder)

	require.Len(t, changes, 0, "expected 0 changes for unmodified object, got %d: %+v", len(changes), changes)
}

func TestPropertyDiff_SliceFields(t *testing.T) {
	vm := &mo.VirtualMachine{
		Guest: &types.GuestInfo{
			Net: []types.GuestNicInfo{
				{
					IpAddress:  []string{"192.168.1.100"},
					MacAddress: "00:50:56:aa:bb:cc",
				},
			},
		},
	}
	vm.Self = types.ManagedObjectReference{Type: "VirtualMachine", Value: "vm-1"}

	checkpoint := Checkpoint(vm)

	// Modify the network info
	vm.Guest.Net[0].IpAddress = []string{"10.0.0.50", "10.0.0.51"}
	vm.Guest.Net[0].MacAddress = "00:50:56:dd:ee:ff"

	changes := PropertyDiff(checkpoint, vm)

	// Should detect change in guest
	for _, c := range changes {
		if c.Name == "guest" {
			return
		}
	}

	t.Error("expected change for 'guest' property containing network changes")
}

func TestCheckpoint(t *testing.T) {
	original := &mo.VirtualMachine{
		ManagedEntity: mo.ManagedEntity{
			Name: "original",
		},
		Guest: &types.GuestInfo{
			IpAddress: "192.168.1.1",
		},
	}
	original.Self = types.ManagedObjectReference{Type: "VirtualMachine", Value: "vm-1"}

	// Checkpoint always returns the embedded mo type as mo.Reference.
	snapshot := Checkpoint(original)
	cp := snapshot.(*mo.VirtualMachine)

	require.NotSame(t, original, cp, "snapshot should be a different pointer")
	assert.Equal(t, original.Name, cp.Name, "snapshot Name mismatch: %q vs %q", cp.Name, original.Name)
	assert.Equal(t, original.Guest.IpAddress, cp.Guest.IpAddress, "snapshot Guest.IpAddress mismatch")

	// Mutate original — snapshot must be unaffected.
	original.Name = "modified"
	original.Guest.IpAddress = "10.0.0.1"

	assert.Equal(t, "original", cp.Name, "snapshot should not reflect later changes to original")
	assert.Equal(t, "192.168.1.1", cp.Guest.IpAddress, "snapshot Guest.IpAddress should not reflect later changes to original")
}

// TestPropertyDiff_WithSimulator tests that ctx.AutoUpdate produces property changes
// visible via the PropertyCollector.
func TestPropertyDiff_WithSimulator(t *testing.T) {
	ctx := context.Background()

	m := VPX()
	defer m.Remove()

	err := m.Create()
	require.NoError(t, err, "expected no error creating simulator")

	s := m.Service.NewServer()
	defer s.Close()

	c, err := govmomi.NewClient(ctx, s.URL, true)
	require.NoError(t, err, "expected no error creating client")

	finder := find.NewFinder(c.Client)
	vm, err := finder.VirtualMachine(ctx, "DC0_H0_VM0")
	require.NoError(t, err, "expected no error retrieving VM")

	simCtx := m.Service.Context
	ref := vm.Reference()
	obj := simCtx.Map.Get(ref).(*VirtualMachine)

	// ctx.AutoUpdate holds the object lock for checkpoint + modifications + diff +
	// update — all in one acquisition, preventing races with concurrent SOAP handlers.
	simCtx.AutoUpdate(obj, func() {
		if obj.Guest == nil {
			obj.Guest = &types.GuestInfo{}
		}
		obj.Guest.IpAddress = "10.20.30.40"
		obj.Guest.HostName = "test-hostname"
	})

	pc := property.DefaultCollector(c.Client)
	var mvm mo.VirtualMachine
	err = pc.RetrieveOne(ctx, ref, []string{"guest.ipAddress", "guest.hostName"}, &mvm)
	require.NoError(t, err, "expected no error retrieving VM properties")

	require.NotNil(t, mvm.Guest, "expected Guest to be set")
	assert.Equal(t, "10.20.30.40", mvm.Guest.IpAddress, "expected IpAddress '10.20.30.40', got %q", mvm.Guest.IpAddress)
	assert.Equal(t, "test-hostname", mvm.Guest.HostName, "expected HostName 'test-hostname', got %q", mvm.Guest.HostName)
}

// TestPropertyDiff_MultipleChanges tests that PropertyDiff correctly handles multiple changes
func TestPropertyDiff_MultipleChanges(t *testing.T) {
	ctx := context.Background()

	m := VPX()
	defer m.Remove()

	err := m.Create()
	require.NoError(t, err, "expected no error creating simulator")

	s := m.Service.NewServer()
	defer s.Close()

	c, err := govmomi.NewClient(ctx, s.URL, true)
	require.NoError(t, err, "expected no error creating client")

	finder := find.NewFinder(c.Client)
	vm, err := finder.VirtualMachine(ctx, "DC0_H0_VM0")
	require.NoError(t, err, "expected no error retrieving VM")

	// Get the simulator's internal VM object
	simCtx := m.Service.Context
	ref := vm.Reference()
	obj := simCtx.Map.Get(ref).(*VirtualMachine)

	// Checkpoint, field writes, PropertyDiff, and Update must all happen
	// under the object lock to prevent races with concurrent SOAP handlers.
	var foundName, foundGuest bool
	simCtx.WithLock(obj, func() {
		checkpoint := Checkpoint(&obj.VirtualMachine)

		obj.Name = "renamed-vm"
		if obj.Guest == nil {
			obj.Guest = &types.GuestInfo{}
		}
		obj.Guest.IpAddress = "99.99.99.99"
		obj.Guest.HostName = "test-hostname"
		obj.Guest.Net = []types.GuestNicInfo{
			{
				IpAddress:  []string{"99.99.99.99", "fe80::1"},
				MacAddress: "00:50:56:aa:bb:cc",
			},
		}

		changes := PropertyDiff(checkpoint, &obj.VirtualMachine)

		for _, c := range changes {
			if c.Name == "name" {
				foundName = true
				assert.Equal(t, "renamed-vm", c.Val, "expected name 'renamed-vm', got %v", c.Val)
			}
			if c.Name == "guest" {
				foundGuest = true
				var guestIP string
				var netLen int
				switch v := c.Val.(type) {
				case *types.GuestInfo:
					guestIP = v.IpAddress
					netLen = len(v.Net)
				case types.GuestInfo:
					guestIP = v.IpAddress
					netLen = len(v.Net)
				default:
					assert.IsType(t, types.GuestInfo{}, c.Val, "expected GuestInfo type, got %T", c.Val)
					return
				}
				assert.Equal(t, "99.99.99.99", guestIP, "expected IpAddress '99.99.99.99', got %q", guestIP)
				assert.Equal(t, 1, netLen, "expected 1 NIC, got %d", netLen)
			}
		}

		simCtx.Update(obj, changes)
	})

	assert.True(t, foundName, "expected change for 'name' property")
	assert.True(t, foundGuest, "expected change for 'guest' property")

	// Verify changes are visible via property collector
	pc := property.DefaultCollector(c.Client)
	var mvm mo.VirtualMachine
	err = pc.RetrieveOne(ctx, ref, []string{"name", "guest"}, &mvm)
	require.NoError(t, err, "expected no error retrieving VM properties")

	assert.Equal(t, "renamed-vm", mvm.Name, "expected Name 'renamed-vm', got %q", mvm.Name)
	require.NotNil(t, mvm.Guest, "expected Guest to be set")
	assert.Equal(t, "99.99.99.99", mvm.Guest.IpAddress, "expected IpAddress '99.99.99.99', got %q", mvm.Guest.IpAddress)
	assert.Equal(t, 1, len(mvm.Guest.Net), "expected 1 NIC, got %d", len(mvm.Guest.Net))
}

func TestDetermineChangeOp(t *testing.T) {
	tests := []struct {
		name     string
		oldVal   interface{}
		newVal   interface{}
		expected types.PropertyChangeOp
	}{
		{
			name:     "nil to value is Add",
			oldVal:   (*string)(nil),
			newVal:   stringPtr("hello"),
			expected: types.PropertyChangeOpAdd,
		},
		{
			name:     "value to nil is Remove",
			oldVal:   stringPtr("hello"),
			newVal:   (*string)(nil),
			expected: types.PropertyChangeOpRemove,
		},
		{
			name:     "value to value is Assign",
			oldVal:   stringPtr("hello"),
			newVal:   stringPtr("world"),
			expected: types.PropertyChangeOpAssign,
		},
		{
			name:     "empty string to non-empty is Add",
			oldVal:   "",
			newVal:   "hello",
			expected: types.PropertyChangeOpAdd,
		},
		{
			name:     "non-empty string to empty is Remove",
			oldVal:   "hello",
			newVal:   "",
			expected: types.PropertyChangeOpRemove,
		},
		{
			name:     "slice changes are Assign (slices are not considered empty)",
			oldVal:   []string{},
			newVal:   []string{"a", "b"},
			expected: types.PropertyChangeOpAssign,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			oldVal := reflect.ValueOf(tt.oldVal)
			newVal := reflect.ValueOf(tt.newVal)
			op := determineChangeOp(oldVal, newVal)
			assert.Equal(t, tt.expected, op, "expected %v, got %v", tt.expected, op)
		})
	}
}

func stringPtr(s string) *string {
	return &s
}

// TestContext_Checkpoint verifies that ctx.Checkpoint returns an independent deep copy
// of the object's mo state, protected by the object lock.
func TestContext_Checkpoint(t *testing.T) {
	m := VPX()
	defer m.Remove()
	require.NoError(t, m.Create())
	s := m.Service.NewServer()
	defer s.Close()

	reg := m.Service.Context.Map
	obj := reg.All("VirtualMachine")[0].(*VirtualMachine)

	simCtx := &Context{Context: context.Background(), Map: reg}

	// Capture a snapshot of the original name.
	simCtx.WithLock(obj, func() {
		obj.Name = "before"
	})

	snapshot := simCtx.Checkpoint(obj)
	snapVM := snapshot.(*mo.VirtualMachine)

	require.Equal(t, "before", snapVM.Name, "snapshot should capture state at checkpoint time")

	// Mutate the live object after taking the snapshot.
	simCtx.WithLock(obj, func() {
		obj.Name = "after"
	})

	assert.Equal(t, "before", snapVM.Name, "snapshot must not be affected by later mutations")
	simCtx.WithLock(obj, func() {
		assert.Equal(t, "after", obj.Name, "live object should have the new name")
	})
}

// TestContext_PropertyDiff verifies that ctx.PropertyDiff computes changes from an old
// snapshot to the current live state and applies them so they are visible via the
// PropertyCollector.
func TestContext_PropertyDiff(t *testing.T) {
	goCtx := context.Background()

	m := VPX()
	defer m.Remove()
	require.NoError(t, m.Create())
	s := m.Service.NewServer()
	defer s.Close()

	c, err := govmomi.NewClient(goCtx, s.URL, true)
	require.NoError(t, err)

	finder := find.NewFinder(c.Client)
	vm, err := finder.VirtualMachine(goCtx, "DC0_H0_VM0")
	require.NoError(t, err)

	simCtx := m.Service.Context
	ref := vm.Reference()
	obj := simCtx.Map.Get(ref).(*VirtualMachine)

	// Take checkpoint, then modify fields and apply diff — all under one lock so
	// field writes are protected alongside the checkpoint and diff reads.
	simCtx.WithLock(obj, func() {
		snapshot := simCtx.Checkpoint(obj) // re-entrant: already holds the lock
		if obj.Guest == nil {
			obj.Guest = &types.GuestInfo{}
		}
		obj.Guest.IpAddress = "1.2.3.4"
		simCtx.UpdateDiff(snapshot, obj) // re-entrant: computes diff, calls Update
	})

	pc := property.DefaultCollector(c.Client)
	var mvm mo.VirtualMachine
	require.NoError(t, pc.RetrieveOne(goCtx, ref, []string{"guest.ipAddress"}, &mvm))
	require.NotNil(t, mvm.Guest)
	assert.Equal(t, "1.2.3.4", mvm.Guest.IpAddress)
}

// TestContext_AutoUpdate verifies that ctx.AutoUpdate correctly applies field changes
// and makes them visible via the PropertyCollector, and is concurrency-safe when
// called from multiple goroutines with independent *Context values.
func TestContext_AutoUpdate(t *testing.T) {
	goCtx := context.Background()

	m := VPX()
	defer m.Remove()
	require.NoError(t, m.Create())
	s := m.Service.NewServer()
	defer s.Close()

	c, err := govmomi.NewClient(goCtx, s.URL, true)
	require.NoError(t, err)

	finder := find.NewFinder(c.Client)
	vm, err := finder.VirtualMachine(goCtx, "DC0_H0_VM0")
	require.NoError(t, err)

	simCtx := m.Service.Context
	ref := vm.Reference()
	obj := simCtx.Map.Get(ref).(*VirtualMachine)

	// Single call: apply changes and verify they are visible.
	simCtx.AutoUpdate(obj, func() {
		if obj.Guest == nil {
			obj.Guest = &types.GuestInfo{}
		}
		obj.Guest.IpAddress = "5.6.7.8"
		obj.Guest.HostName = "autohost"
	})

	pc := property.DefaultCollector(c.Client)
	var mvm mo.VirtualMachine
	require.NoError(t, pc.RetrieveOne(goCtx, ref, []string{"guest.ipAddress", "guest.hostName"}, &mvm))
	require.NotNil(t, mvm.Guest)
	assert.Equal(t, "5.6.7.8", mvm.Guest.IpAddress)
	assert.Equal(t, "autohost", mvm.Guest.HostName)

	// Concurrent calls: two goroutines, each with their own *Context, using AutoUpdate.
	reg := m.Service.Context.Map
	newCtx := func() *Context { return &Context{Context: context.Background(), Map: reg} }

	const iters = 100
	var wg sync.WaitGroup
	for g := 0; g < 2; g++ {
		wg.Add(1)
		g := g
		go func() {
			defer wg.Done()
			ctx := newCtx()
			for i := 0; i < iters; i++ {
				ctx.AutoUpdate(obj, func() {
					if obj.Guest == nil {
						obj.Guest = &types.GuestInfo{}
					}
					obj.Guest.IpAddress = fmt.Sprintf("10.%d.%d.1", g, i)
				})
			}
		}()
	}
	wg.Wait()
}

// TestPropertyDiff_RuntimePowerState tests that runtime.powerState changes are properly tracked
func TestPropertyDiff_RuntimePowerState(t *testing.T) {
	vm := &mo.VirtualMachine{
		Runtime: types.VirtualMachineRuntimeInfo{
			PowerState: types.VirtualMachinePowerStatePoweredOff,
		},
	}
	vm.Self = types.ManagedObjectReference{Type: "VirtualMachine", Value: "vm-1"}

	checkpoint := Checkpoint(vm)

	// Change power state
	vm.Runtime.PowerState = types.VirtualMachinePowerStatePoweredOn

	changes := PropertyDiff(checkpoint, vm)

	for _, c := range changes {
		if c.Name == "runtime" {
			runtime, ok := c.Val.(types.VirtualMachineRuntimeInfo)
			assert.True(t, ok, "expected types.VirtualMachineRuntimeInfo, got %T", c.Val)
			assert.Equal(t, types.VirtualMachinePowerStatePoweredOn, runtime.PowerState, "expected PowerState PoweredOn")
			return
		}
	}

	t.Error("expected change for 'runtime' property")
}

// TestPropertyDiff_GuestNetInfo tests that guest.net changes produce correct property changes
func TestPropertyDiff_GuestNetInfo(t *testing.T) {
	vm := &mo.VirtualMachine{
		Guest: &types.GuestInfo{
			Net: []types.GuestNicInfo{},
		},
	}
	vm.Self = types.ManagedObjectReference{Type: "VirtualMachine", Value: "vm-1"}

	checkpoint := Checkpoint(vm)

	// Add network info (simulating container network detection)
	vm.Guest.Net = []types.GuestNicInfo{
		{
			Network:    "bridge",
			IpAddress:  []string{"172.17.0.2", "fe80::42:acff:fe11:2"},
			MacAddress: "02:42:ac:11:00:02",
			Connected:  true,
		},
	}
	vm.Guest.IpAddress = "172.17.0.2"
	vm.Guest.HostName = "container-hostname"

	changes := PropertyDiff(checkpoint, vm)
	require.Greater(t, len(changes), 0, "expected property changes for guest network info")

	foundGuest := false
	for _, c := range changes {
		if c.Name == "guest" {
			foundGuest = true
			// Verify the guest info contains network data
			var guestInfo *types.GuestInfo
			switch v := c.Val.(type) {
			case *types.GuestInfo:
				guestInfo = v
			case types.GuestInfo:
				guestInfo = &v
			}

			if assert.NotNil(t, guestInfo, "expected GuestInfo, got %T", c.Val) {
				continue
			}

			require.Equal(t, 1, len(guestInfo.Net), "expected 1 NIC, got %d", len(guestInfo.Net))
			assert.Equal(t, "172.17.0.2", guestInfo.IpAddress, "expected IpAddress '172.17.0.2'")
		}
	}

	assert.True(t, foundGuest, "expected change for 'guest' property")
}

// TestPropertyDiff_SummaryGuest tests that summary.guest changes are tracked
func TestPropertyDiff_SummaryGuest(t *testing.T) {
	vm := &mo.VirtualMachine{
		Summary: types.VirtualMachineSummary{
			Guest: &types.VirtualMachineGuestSummary{},
		},
	}
	vm.Self = types.ManagedObjectReference{Type: "VirtualMachine", Value: "vm-1"}

	checkpoint := Checkpoint(vm)

	// Update summary guest info
	vm.Summary.Guest.IpAddress = "10.0.0.100"
	vm.Summary.Guest.HostName = "test-host"

	changes := PropertyDiff(checkpoint, vm)

	for _, c := range changes {
		if c.Name == "summary" {
			return
		}
	}

	t.Error("expected change for 'summary' property")
}

// TestPropertyDiff_ConcurrentAccess is a race regression test for the pattern used in
// syncNetworkConfigToVMGuestProperties. It verifies that ctx.AutoUpdate (and the
// underlying checkpoint → modify → diff → update sequence) is safe when called
// concurrently from multiple goroutines, each using its own *Context.
//
// Note: each goroutine must use its own *Context value (not a shared pointer) because
// ObjectLock uses the context pointer as the lock-identity for re-entrancy detection.
// Two goroutines sharing the same *Context would bypass mutual exclusion.
//
// Run with -race to confirm no data races:
//
//	go test -race -run TestPropertyDiff_ConcurrentAccess ./simulator/
func TestPropertyDiff_ConcurrentAccess(t *testing.T) {
	m := VPX()
	defer m.Remove()

	err := m.Create()
	require.NoError(t, err, "failed to create VPX model")

	s := m.Service.NewServer()
	defer s.Close()

	reg := m.Service.Context.Map
	obj := reg.All("VirtualMachine")[0].(*VirtualMachine)

	// Each goroutine gets its own Context so ObjectLock's re-entrancy check
	// correctly serialises the goroutines rather than treating them as the same holder.
	newCtx := func() *Context {
		return &Context{Context: context.Background(), Map: reg}
	}

	const iters = 200
	var wg sync.WaitGroup

	// Goroutine A: simulates a background watcher (e.g. watchContainer callback).
	// ctx.AutoUpdate holds a single lock for checkpoint + modify + diff + update.
	wg.Add(1)
	go func() {
		defer wg.Done()
		ctx := newCtx()
		for i := 0; i < iters; i++ {
			ctx.AutoUpdate(obj, func() {
				if obj.Guest == nil {
					obj.Guest = &types.GuestInfo{}
				}
				obj.Guest.IpAddress = fmt.Sprintf("10.0.0.%d", i)
			})
		}
	}()

	// Goroutine B: simulates a concurrent SOAP handler that modifies VM fields
	// under the object lock, as all SOAP task handlers do via Task.Run → AcquireLock.
	wg.Add(1)
	go func() {
		defer wg.Done()
		ctx := newCtx()
		for i := 0; i < iters; i++ {
			ctx.WithLock(obj, func() {
				if obj.Guest == nil {
					obj.Guest = &types.GuestInfo{}
				}
				obj.Guest.HostName = fmt.Sprintf("host-%d", i)
			})
		}
	}()

	wg.Wait()
}
