// Copyright 2022 Hugo Stijns
// SPDX-License-Identifier: MIT

package model

import (
	"context"
	"github.com/pb33f/libopenapi/index"
	"testing"

	"github.com/pb33f/libopenapi/datamodel/low"
	"github.com/pb33f/libopenapi/datamodel/low/base"
	"github.com/pb33f/libopenapi/orderedmap"
	"github.com/pb33f/libopenapi/utils"
	"github.com/stretchr/testify/assert"
	"go.yaml.in/yaml/v4"
)

func Test_CheckForModification(t *testing.T) {
	tests := []struct {
		name   string
		left   string
		right  string
		differ bool
	}{
		{"Same string quoted", `value`, `"value"`, false},
		{"Same string", `value`, `value`, false},
		{"Same boolean", `true`, `true`, false},
		{"Equivalent exponent formatting", `1e-08`, `1e-8`, false},
		{"Equivalent integer and float formatting", `1`, `1.0`, false},
		{"Equivalent decimal formatting", `0.10`, `0.1`, false},
		{"Different boolean", `true`, `false`, true},
		{"Different string", `value_a`, `value_b`, true},
		{"Different int", `123`, `"123"`, true},
		{"Different float", `123.456`, `"123.456"`, true},
		{"Different boolean quoted", `true`, `"true"`, true},
		{"Different date", `2022-12-29`, `"2022-12-29"`, true},
		{"Different value and tag", `2.0`, `2.0.0`, true},
		{"From null to empty value", `null`, `""`, false},
		{"From empty value to null", `""`, `null`, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var lNode, rNode yaml.Node
			_ = yaml.Unmarshal([]byte(tt.left), &lNode)
			_ = yaml.Unmarshal([]byte(tt.right), &rNode)

			changes := []*Change{}
			CheckForModification(lNode.Content[0], rNode.Content[0], "test", &changes, false, "old", "new")

			if tt.differ {
				assert.Len(t, changes, 1)
			} else {
				assert.Empty(t, changes)
			}
		})
	}
}

func Test_CheckForModification_ArrayMap(t *testing.T) {
	tests := []struct {
		name   string
		left   any
		right  any
		differ bool
	}{
		{"Same slice", []string{"cake"}, []string{"cake"}, false},
		{"bigger slice right", []string{"cake"}, []string{"cake", "cheese"}, true},
		{"bigger slice left", []string{"cake", "burgers"}, []string{"cake"}, true},
		{"different slice left", "cake", []string{"cake"}, true},
		{"different slice right", []string{"cake"}, "cake", true},
		{"different slice value", []string{"cake"}, []string{"burgers"}, true},
		{"same map", map[string]string{"pie": "cake"}, map[string]string{"pie": "cake"}, false},
		{"different map", map[string]string{"pie": "cake"}, map[string]string{"pizza": "burgers"}, true},
		{"different map left", "pie", map[string]string{"pie": "cake"}, true},
		{"different map right", map[string]string{"pie": "cake"}, "burgers", true},
		{"different map", map[string]string{"pie": "cake"}, map[string]string{"pizza": "time"}, true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var lNode, rNode yaml.Node
			encA, _ := yaml.Marshal(tt.left)
			encB, _ := yaml.Marshal(tt.right)
			_ = yaml.Unmarshal(encA, &lNode)
			_ = yaml.Unmarshal(encB, &rNode)

			changes := []*Change{}
			CheckForModification(lNode.Content[0], rNode.Content[0], tt.name, &changes, false, "old", "new")

			if tt.differ {
				assert.Len(t, changes, 1)
			} else {
				assert.Empty(t, changes)
			}
		})
	}
}

func Test_CheckMapsRemoval(t *testing.T) {
	mapA := make(map[string]string)
	mapB := make(map[string]string)

	mapA["key"] = "value"
	mapB["key"] = "shmalue"

	tests := []struct {
		name   string
		left   map[string]string
		right  map[string]string
		differ bool
	}{
		{"Same map", mapA, mapB, false},
		{"Different map", mapA, mapB, false},
		{"Add map", nil, mapA, false},
		{"Remove map", mapA, nil, true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var lNode, rNode yaml.Node
			encA, _ := yaml.Marshal(tt.left)
			encB, _ := yaml.Marshal(tt.right)
			_ = yaml.Unmarshal(encA, &lNode)
			_ = yaml.Unmarshal(encB, &rNode)

			changes := []*Change{}

			r := rNode.Content[0]
			if len(r.Content) == 0 {
				r = nil
			}
			CheckForRemoval(lNode.Content[0], r, "test", &changes, false, "old", "new")

			if tt.differ {
				assert.Len(t, changes, 1)
			} else {
				assert.Empty(t, changes)
			}
		})
	}
}

func Test_CheckMapsAddition(t *testing.T) {
	mapA := make(map[string]string)
	mapB := make(map[string]string)

	mapA["key"] = "value"
	mapB["key"] = "shmalue"

	tests := []struct {
		name   string
		left   map[string]string
		right  map[string]string
		differ bool
		empty  bool
	}{
		{"Same map", mapA, mapB, false, false},
		{"Different map", mapA, mapB, false, false},
		{"Add map", nil, mapA, true, false},
		{"Add map value", nil, mapB, true, true},
		{"Remove map", mapA, nil, false, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var lNode, rNode yaml.Node
			encA, _ := yaml.Marshal(tt.left)
			encB, _ := yaml.Marshal(tt.right)
			_ = yaml.Unmarshal(encA, &lNode)
			_ = yaml.Unmarshal(encB, &rNode)

			changes := []*Change{}

			r := rNode.Content[0]
			l := lNode.Content[0]
			if len(r.Content) == 0 {
				r = nil
			}
			if len(l.Content) == 0 {
				if !tt.empty {
					l = nil
				}
			}
			CheckForAddition(l, r, "test", &changes, false, "old", "new")

			if tt.differ {
				assert.Len(t, changes, 1)
			} else {
				assert.Empty(t, changes)
			}
		})
	}
}

func TestFlattenLowLevelOrderedMap(t *testing.T) {
	m := orderedmap.New[low.KeyReference[string], low.ValueReference[string]]()
	m.Set(low.KeyReference[string]{
		Value: "key",
	}, low.ValueReference[string]{
		Value: "value",
	})

	o := FlattenLowLevelOrderedMap[string](m)
	assert.Equal(t, map[string]*low.ValueReference[string]{
		"key": {
			Value: "value",
		},
	}, o)
}

func TestCheckMapForAdditionRemoval(t *testing.T) {
	var changes []*Change

	l := orderedmap.New[low.KeyReference[string], low.ValueReference[string]]()
	l.Set(low.KeyReference[string]{
		Value: "key",
	}, low.ValueReference[string]{
		Value:     "value",
		ValueNode: utils.CreateStringNode("value"),
	})
	l.Set(low.KeyReference[string]{
		Value: "key2",
	}, low.ValueReference[string]{
		Value:     "value2",
		ValueNode: utils.CreateStringNode("value2"),
	})

	r := orderedmap.New[low.KeyReference[string], low.ValueReference[string]]()
	r.Set(low.KeyReference[string]{
		Value: "key",
	}, low.ValueReference[string]{
		Value:     "value",
		ValueNode: utils.CreateStringNode("value"),
	})

	CheckMapForAdditionRemoval(l, r, &changes, "label")
	assert.Len(t, changes, 1)
}

func buildTestExample(t *testing.T, raw string) *base.Example {
	t.Helper()

	var node yaml.Node
	assert.NoError(t, yaml.Unmarshal([]byte(raw), &node))

	var example base.Example
	assert.NoError(t, low.BuildModel(node.Content[0], &example))
	assert.NoError(t, example.Build(context.Background(), nil, node.Content[0], nil))
	return &example
}

func TestOverrideChangeCollectionBreaking(t *testing.T) {
	overrideChangeCollectionBreaking(nil, true)
	overrideChangeCollectionBreaking("not-a-collection", true)

	changes := &ExampleChanges{
		PropertyChanges: NewPropertyChanges([]*Change{
			{Breaking: true},
			{Breaking: false},
		}),
	}

	overrideChangeCollectionBreaking(changes, false)
	assert.False(t, changes.Changes[0].Breaking)
	assert.False(t, changes.Changes[1].Breaking)

	overrideChangeCollectionBreaking(changes, true)
	assert.True(t, changes.Changes[0].Breaking)
	assert.True(t, changes.Changes[1].Breaking)
}

func TestCompareExamplesWithParentBreaking(t *testing.T) {
	ResetDefaultBreakingRules()
	ResetActiveBreakingRulesConfig()
	defer ResetDefaultBreakingRules()
	defer ResetActiveBreakingRulesConfig()

	config := GenerateDefaultBreakingRules()
	config.Example.Value = rule(false, true, false)
	config.Parameter.Examples = rule(false, false, false)
	config.MediaType.Examples = rule(true, false, true)
	SetActiveBreakingRulesConfig(config)

	modified := compareExamplesWithParentBreaking(CompParameter, PropExamples)(
		buildTestExample(t, "value: left"),
		buildTestExample(t, "value: right"),
	)
	assert.NotNil(t, modified)
	assert.False(t, modified.GetAllChanges()[0].Breaking)

	added := compareExamplesWithParentBreaking(CompMediaType, PropExamples)(
		nil,
		buildTestExample(t, "value: right"),
	)
	assert.NotNil(t, added)
	assert.True(t, added.GetAllChanges()[0].Breaking)

	removed := compareExamplesWithParentBreaking(CompMediaType, PropExamples)(
		buildTestExample(t, "value: left"),
		nil,
	)
	assert.NotNil(t, removed)
	assert.True(t, removed.GetAllChanges()[0].Breaking)

	unchanged := compareExamplesWithParentBreaking(CompParameter, PropExamples)(
		buildTestExample(t, "value: same"),
		buildTestExample(t, "value: same"),
	)
	assert.Nil(t, unchanged)
}

type test_hasIndex struct {
	index *index.SpecIndex
}

func (t *test_hasIndex) GetIndex() *index.SpecIndex {
	return t.index
}

func Test_checkLocation(t *testing.T) {

	idx := index.NewSpecIndex(&yaml.Node{Content: []*yaml.Node{{Content: []*yaml.Node{}}}})
	idxB := index.NewSpecIndex(&yaml.Node{Content: []*yaml.Node{{Content: []*yaml.Node{}}}})
	rolodex := index.NewRolodex(&index.SpecIndexConfig{})
	idx.SetRolodex(rolodex)
	idxB.SetRolodex(rolodex)
	rolodex.SetRootIndex(idx)

	testHasIndex := &test_hasIndex{
		index: idxB,
	}

	// https://suno.com/s/FtPAc2SaXEw5vTsH
	idxB.SetAbsolutePath("milly-milk-bottle")
	assert.True(t, checkLocation(&ChangeContext{DocumentLocation: "sunny-spain"}, testHasIndex))

}

func Test_checkLocation_sameIdx(t *testing.T) {

	idx := index.NewSpecIndex(&yaml.Node{Content: []*yaml.Node{{Content: []*yaml.Node{}}}})
	rolodex := index.NewRolodex(&index.SpecIndexConfig{})
	idx.SetRolodex(rolodex)
	rolodex.SetRootIndex(idx)

	testHasIndex := &test_hasIndex{
		index: idx,
	}
	idx.SetAbsolutePath("milly-milk-bottle")
	assert.False(t, checkLocation(&ChangeContext{DocumentLocation: ""}, testHasIndex))

}

// TestSetReferenceIfExists tests the SetReferenceIfExists function
func TestSetReferenceIfExists(t *testing.T) {
	tests := []struct {
		name           string
		setupValue     func() *low.ValueReference[string]
		setupChangeObj func() any
		expectRef      string
	}{
		{
			name: "sets reference when value has reference and object implements interface",
			setupValue: func() *low.ValueReference[string] {
				val := low.ValueReference[string]{
					Value: "test",
				}
				val.SetReference("#/components/headers/TestHeader", nil)
				return &val
			},
			setupChangeObj: func() any {
				return &PropertyChanges{}
			},
			expectRef: "#/components/headers/TestHeader",
		},
		{
			name: "does not set reference when value has no reference",
			setupValue: func() *low.ValueReference[string] {
				val := low.ValueReference[string]{
					Value: "test",
				}
				return &val
			},
			setupChangeObj: func() any {
				return &PropertyChanges{}
			},
			expectRef: "",
		},
		{
			name:       "handles nil value without panic",
			setupValue: func() *low.ValueReference[string] { return nil },
			setupChangeObj: func() any {
				return &PropertyChanges{}
			},
			expectRef: "",
		},
		{
			name: "handles non-ChangeIsReferenced object without panic",
			setupValue: func() *low.ValueReference[string] {
				val := low.ValueReference[string]{
					Value: "test",
				}
				val.SetReference("#/components/headers/TestHeader", nil)
				return &val
			},
			setupChangeObj: func() any {
				// Return something that doesn't implement ChangeIsReferenced
				return &struct{}{}
			},
			expectRef: "",
		},
		{
			name: "sets reference for remote reference",
			setupValue: func() *low.ValueReference[string] {
				val := low.ValueReference[string]{
					Value: "test",
				}
				val.SetReference("./schemas.yaml#/components/schemas/TestSchema", nil)
				return &val
			},
			setupChangeObj: func() any {
				return &PropertyChanges{}
			},
			expectRef: "./schemas.yaml#/components/schemas/TestSchema",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			value := tt.setupValue()
			changeObj := tt.setupChangeObj()

			// Call the function
			SetReferenceIfExists(value, changeObj)

			// Check the result if it implements ChangeIsReferenced
			if refObj, ok := changeObj.(ChangeIsReferenced); ok {
				assert.Equal(t, tt.expectRef, refObj.GetChangeReference())
			}
		})
	}
}

// TestSetReferenceIfExists_Integration tests the integration with actual comparison scenarios
func TestSetReferenceIfExists_Integration(t *testing.T) {
	// Create a referenced value
	valueNode := &yaml.Node{
		Kind:  yaml.ScalarNode,
		Value: "test-value",
	}

	refValue := low.ValueReference[string]{
		Value:     "test-value",
		ValueNode: valueNode,
	}
	refValue.SetReference("#/components/parameters/TestParam", nil)

	// Test with HeaderChanges (which embeds PropertyChanges)
	t.Run("HeaderChanges with reference", func(t *testing.T) {
		headerChanges := &HeaderChanges{
			PropertyChanges: &PropertyChanges{},
		}

		SetReferenceIfExists(&refValue, headerChanges)
		assert.Equal(t, "#/components/parameters/TestParam", headerChanges.GetChangeReference())
	})

	// Test with SchemaChanges
	t.Run("SchemaChanges with reference", func(t *testing.T) {
		schemaChanges := &SchemaChanges{
			PropertyChanges: &PropertyChanges{},
		}

		SetReferenceIfExists(&refValue, schemaChanges)
		assert.Equal(t, "#/components/parameters/TestParam", schemaChanges.GetChangeReference())
	})

	// Test that it preserves existing references
	t.Run("preserves existing reference", func(t *testing.T) {
		changes := &PropertyChanges{
			ChangeReference: "#/existing/reference",
		}

		// Create a value without reference
		nonRefValue := low.ValueReference[string]{
			Value: "test",
		}

		SetReferenceIfExists(&nonRefValue, changes)
		// Should still have the existing reference
		assert.Equal(t, "#/existing/reference", changes.GetChangeReference())
	})

	// Test overwriting reference
	t.Run("overwrites reference when new reference exists", func(t *testing.T) {
		changes := &PropertyChanges{
			ChangeReference: "#/old/reference",
		}

		SetReferenceIfExists(&refValue, changes)
		// Should have the new reference
		assert.Equal(t, "#/components/parameters/TestParam", changes.GetChangeReference())
	})
}

// TestPreserveParameterReference tests the PreserveParameterReference helper function
func TestPreserveParameterReference(t *testing.T) {
	t.Run("preserves left reference when left has ref", func(t *testing.T) {
		lRef := low.ValueReference[string]{Value: "left"}
		lRef.SetReference("#/components/parameters/LeftParam", nil)
		rRef := low.ValueReference[string]{Value: "right"}

		lRefs := map[string]*low.ValueReference[string]{"param": &lRef}
		rRefs := map[string]*low.ValueReference[string]{"param": &rRef}

		changes := &ParameterChanges{PropertyChanges: &PropertyChanges{}}
		PreserveParameterReference(lRefs, rRefs, "param", changes)

		assert.Equal(t, "#/components/parameters/LeftParam", changes.GetChangeReference())
	})

	t.Run("preserves right reference when only right has ref", func(t *testing.T) {
		lRef := low.ValueReference[string]{Value: "left"}
		rRef := low.ValueReference[string]{Value: "right"}
		rRef.SetReference("#/components/parameters/RightParam", nil)

		lRefs := map[string]*low.ValueReference[string]{"param": &lRef}
		rRefs := map[string]*low.ValueReference[string]{"param": &rRef}

		changes := &ParameterChanges{PropertyChanges: &PropertyChanges{}}
		PreserveParameterReference(lRefs, rRefs, "param", changes)

		assert.Equal(t, "#/components/parameters/RightParam", changes.GetChangeReference())
	})

	t.Run("handles missing parameter gracefully", func(t *testing.T) {
		lRefs := map[string]*low.ValueReference[string]{}
		rRefs := map[string]*low.ValueReference[string]{}

		changes := &ParameterChanges{PropertyChanges: &PropertyChanges{}}
		PreserveParameterReference(lRefs, rRefs, "missing", changes)

		assert.Equal(t, "", changes.GetChangeReference())
	})

	t.Run("prefers left reference over right", func(t *testing.T) {
		lRef := low.ValueReference[string]{Value: "left"}
		lRef.SetReference("#/components/parameters/LeftParam", nil)
		rRef := low.ValueReference[string]{Value: "right"}
		rRef.SetReference("#/components/parameters/RightParam", nil)

		lRefs := map[string]*low.ValueReference[string]{"param": &lRef}
		rRefs := map[string]*low.ValueReference[string]{"param": &rRef}

		changes := &ParameterChanges{PropertyChanges: &PropertyChanges{}}
		PreserveParameterReference(lRefs, rRefs, "param", changes)

		// Left takes priority
		assert.Equal(t, "#/components/parameters/LeftParam", changes.GetChangeReference())
	})
}

// TestCheckForRemovalWithEncoding tests the removal check with encoding
func TestCheckForRemovalWithEncoding(t *testing.T) {
	t.Run("detects removal with encoding", func(t *testing.T) {
		var lNode yaml.Node
		_ = yaml.Unmarshal([]byte(`key: value`), &lNode)

		changes := []*Change{}
		CheckForRemovalWithEncoding(lNode.Content[0], nil, "test", &changes, false, "old", "new")

		assert.Len(t, changes, 1)
		assert.Equal(t, PropertyRemoved, changes[0].ChangeType)
	})

	t.Run("no change when both nodes present", func(t *testing.T) {
		var lNode, rNode yaml.Node
		_ = yaml.Unmarshal([]byte(`value`), &lNode)
		_ = yaml.Unmarshal([]byte(`value`), &rNode)

		changes := []*Change{}
		CheckForRemovalWithEncoding(lNode.Content[0], rNode.Content[0], "test", &changes, false, "old", "new")

		assert.Empty(t, changes)
	})
}

// TestCheckForAdditionWithEncoding tests the addition check with encoding
func TestCheckForAdditionWithEncoding(t *testing.T) {
	t.Run("detects addition with encoding", func(t *testing.T) {
		var rNode yaml.Node
		_ = yaml.Unmarshal([]byte(`newvalue`), &rNode)

		changes := []*Change{}
		CheckForAdditionWithEncoding[string](nil, rNode.Content[0], "test", &changes, false, "", "new")

		assert.Len(t, changes, 1)
		assert.Equal(t, PropertyAdded, changes[0].ChangeType)
	})

	t.Run("detects array addition with encoding", func(t *testing.T) {
		var rNode yaml.Node
		_ = yaml.Unmarshal([]byte(`[item1, item2]`), &rNode)

		changes := []*Change{}
		CheckForAdditionWithEncoding[string](nil, rNode.Content[0], "test", &changes, false, "", "new")

		assert.Len(t, changes, 1)
		assert.Equal(t, PropertyAdded, changes[0].ChangeType)
		assert.NotEmpty(t, changes[0].NewEncoded)
	})
}

// TestCheckForModificationWithEncoding tests the modification check with encoding
func TestCheckForModificationWithEncoding(t *testing.T) {
	tests := []struct {
		name   string
		left   any
		right  any
		differ bool
	}{
		{"scalar to different scalar", "value", "changed", true},
		{"array to non-array", []string{"a"}, "scalar", true},
		{"non-array to array", "scalar", []string{"a"}, true},
		{"map to non-map", map[string]string{"k": "v"}, "scalar", true},
		{"non-map to map", "scalar", map[string]string{"k": "v"}, true},
		{"same array", []string{"a"}, []string{"a"}, false},
		{"different array content", []string{"a"}, []string{"b"}, true},
		{"different array length", []string{"a"}, []string{"a", "b"}, true},
		{"same map", map[string]string{"k": "v"}, map[string]string{"k": "v"}, false},
		{"different map", map[string]string{"k": "v"}, map[string]string{"k": "x"}, true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var lNode, rNode yaml.Node
			encL, _ := yaml.Marshal(tt.left)
			encR, _ := yaml.Marshal(tt.right)
			_ = yaml.Unmarshal(encL, &lNode)
			_ = yaml.Unmarshal(encR, &rNode)

			changes := []*Change{}
			CheckForModificationWithEncoding(lNode.Content[0], rNode.Content[0], tt.name, &changes, false, "old", "new")

			if tt.differ {
				assert.Len(t, changes, 1, "expected 1 change for %s", tt.name)
			} else {
				assert.Empty(t, changes, "expected no changes for %s", tt.name)
			}
		})
	}
}

func TestCheckForModificationWithEncoding_NumericScalarEquivalence(t *testing.T) {
	var lNode, rNode yaml.Node
	_ = yaml.Unmarshal([]byte(`1e-08`), &lNode)
	_ = yaml.Unmarshal([]byte(`1e-8`), &rNode)

	changes := []*Change{}
	CheckForModificationWithEncoding(lNode.Content[0], rNode.Content[0], "numeric", &changes, false, "old", "new")

	assert.Empty(t, changes)
}

// TestCheckMapForChangesWithComp tests the deprecated CheckMapForChangesWithComp function
func TestCheckMapForChangesWithComp(t *testing.T) {
	t.Run("detects removal", func(t *testing.T) {
		l := orderedmap.New[low.KeyReference[string], low.ValueReference[string]]()
		l.Set(low.KeyReference[string]{Value: "key1"}, low.ValueReference[string]{
			Value:     "value1",
			ValueNode: utils.CreateStringNode("value1"),
		})

		r := orderedmap.New[low.KeyReference[string], low.ValueReference[string]]()

		var changes []*Change
		result := CheckMapForChangesWithComp(l, r, &changes, "test", func(l, r string) *string { return nil }, false)

		assert.Len(t, changes, 1)
		assert.Equal(t, ObjectRemoved, changes[0].ChangeType)
		assert.Empty(t, result)
	})

	t.Run("detects addition", func(t *testing.T) {
		l := orderedmap.New[low.KeyReference[string], low.ValueReference[string]]()

		r := orderedmap.New[low.KeyReference[string], low.ValueReference[string]]()
		r.Set(low.KeyReference[string]{Value: "key1"}, low.ValueReference[string]{
			Value:     "value1",
			ValueNode: utils.CreateStringNode("value1"),
		})

		var changes []*Change
		result := CheckMapForChangesWithComp(l, r, &changes, "test", func(l, r string) *string { return nil }, false)

		assert.Len(t, changes, 1)
		assert.Equal(t, ObjectAdded, changes[0].ChangeType)
		assert.Empty(t, result)
	})

	t.Run("detects modification with compare enabled", func(t *testing.T) {
		l := orderedmap.New[low.KeyReference[string], low.ValueReference[string]]()
		l.Set(low.KeyReference[string]{Value: "key1"}, low.ValueReference[string]{
			Value:     "value1",
			ValueNode: utils.CreateStringNode("value1"),
		})

		r := orderedmap.New[low.KeyReference[string], low.ValueReference[string]]()
		r.Set(low.KeyReference[string]{Value: "key1"}, low.ValueReference[string]{
			Value:     "value2",
			ValueNode: utils.CreateStringNode("value2"),
		})

		var changes []*Change
		compareResult := "compared"
		result := CheckMapForChangesWithComp(l, r, &changes, "test", func(l, r string) *string { return &compareResult }, true)

		assert.Len(t, result, 1)
		assert.Equal(t, "compared", *result["key1"])
	})

	t.Run("handles nil maps", func(t *testing.T) {
		var changes []*Change
		result := CheckMapForChangesWithComp[string, *string](nil, nil, &changes, "test", func(l, r string) *string { return nil }, false)

		assert.Empty(t, changes)
		assert.Empty(t, result)
	})
}

// TestCheckPropertiesWithEncoding_Component tests the Component branch of CheckPropertiesWithEncoding
func TestCheckPropertiesWithEncoding_Component(t *testing.T) {
	// Reset breaking rules to ensure deterministic results
	ResetDefaultBreakingRules()
	ResetActiveBreakingRulesConfig()
	defer func() {
		ResetActiveBreakingRulesConfig()
		ResetDefaultBreakingRules()
	}()

	t.Run("uses configurable breaking rules when Component is set", func(t *testing.T) {
		var lNode, rNode yaml.Node
		_ = yaml.Unmarshal([]byte(`oldvalue`), &lNode)
		_ = yaml.Unmarshal([]byte(`newvalue`), &rNode)

		var changes []*Change
		props := []*PropertyCheck{
			{
				LeftNode:  lNode.Content[0],
				RightNode: rNode.Content[0],
				Label:     "test",
				Changes:   &changes,
				Breaking:  false,
				Original:  "old",
				New:       "new",
				Component: CompTag,
				Property:  PropName,
			},
		}

		CheckPropertiesWithEncoding(props)

		assert.Len(t, changes, 1)
		assert.Equal(t, Modified, changes[0].ChangeType)
	})

	t.Run("detects removal with Component set", func(t *testing.T) {
		var lNode yaml.Node
		_ = yaml.Unmarshal([]byte(`oldvalue`), &lNode)

		var changes []*Change
		props := []*PropertyCheck{
			{
				LeftNode:  lNode.Content[0],
				RightNode: nil,
				Label:     "test",
				Changes:   &changes,
				Breaking:  false,
				Original:  "old",
				New:       "",
				Component: CompTag,
				Property:  PropName,
			},
		}

		CheckPropertiesWithEncoding(props)

		assert.Len(t, changes, 1)
		assert.Equal(t, PropertyRemoved, changes[0].ChangeType)
	})

	t.Run("detects addition with Component set", func(t *testing.T) {
		var rNode yaml.Node
		_ = yaml.Unmarshal([]byte(`newvalue`), &rNode)

		var changes []*Change
		props := []*PropertyCheck{
			{
				LeftNode:  nil,
				RightNode: rNode.Content[0],
				Label:     "test",
				Changes:   &changes,
				Breaking:  false,
				Original:  "",
				New:       "new",
				Component: CompTag,
				Property:  PropName,
			},
		}

		CheckPropertiesWithEncoding(props)

		assert.Len(t, changes, 1)
		assert.Equal(t, PropertyAdded, changes[0].ChangeType)
	})

	t.Run("falls back to Breaking field when Component not set", func(t *testing.T) {
		var lNode, rNode yaml.Node
		_ = yaml.Unmarshal([]byte(`oldvalue`), &lNode)
		_ = yaml.Unmarshal([]byte(`newvalue`), &rNode)

		var changes []*Change
		props := []*PropertyCheck{
			{
				LeftNode:  lNode.Content[0],
				RightNode: rNode.Content[0],
				Label:     "test",
				Changes:   &changes,
				Breaking:  true,
				Original:  "old",
				New:       "new",
				Component: "", // No component set
				Property:  "",
			},
		}

		CheckPropertiesWithEncoding(props)

		assert.Len(t, changes, 1)
		assert.True(t, changes[0].Breaking)
	})
}

// TestCreateChangeWithEncoding_ComplexValues tests encoding behavior
func TestCreateChangeWithEncoding_ComplexValues(t *testing.T) {
	t.Run("encodes map values", func(t *testing.T) {
		var lNode yaml.Node
		_ = yaml.Unmarshal([]byte(`{key: value, nested: {a: b}}`), &lNode)

		changes := []*Change{}
		CreateChangeWithEncoding(&changes, Modified, "test", lNode.Content[0], nil, false, nil, nil)

		assert.Len(t, changes, 1)
		assert.NotEmpty(t, changes[0].OriginalEncoded)
	})

	t.Run("encodes array values", func(t *testing.T) {
		var rNode yaml.Node
		_ = yaml.Unmarshal([]byte(`[item1, item2, item3]`), &rNode)

		changes := []*Change{}
		CreateChangeWithEncoding(&changes, Modified, "test", nil, rNode.Content[0], false, nil, nil)

		assert.Len(t, changes, 1)
		assert.NotEmpty(t, changes[0].NewEncoded)
	})

	t.Run("does not encode scalar values", func(t *testing.T) {
		var lNode, rNode yaml.Node
		_ = yaml.Unmarshal([]byte(`scalar`), &lNode)
		_ = yaml.Unmarshal([]byte(`another`), &rNode)

		changes := []*Change{}
		CreateChangeWithEncoding(&changes, Modified, "test", lNode.Content[0], rNode.Content[0], false, nil, nil)

		assert.Len(t, changes, 1)
		assert.Empty(t, changes[0].OriginalEncoded)
		assert.Empty(t, changes[0].NewEncoded)
	})
}

// TestCheckPropertyAdditionOrRemoval tests the combined addition/removal check (lines 322-328)
func TestCheckPropertyAdditionOrRemoval(t *testing.T) {
	t.Run("detects removal when left exists but right is nil", func(t *testing.T) {
		var lNode yaml.Node
		_ = yaml.Unmarshal([]byte(`oldvalue`), &lNode)

		changes := []*Change{}
		CheckPropertyAdditionOrRemoval[string](lNode.Content[0], nil, "test", &changes, true, "old", "")

		assert.Len(t, changes, 1)
		assert.Equal(t, PropertyRemoved, changes[0].ChangeType)
		assert.True(t, changes[0].Breaking)
	})

	t.Run("detects addition when left is nil but right exists", func(t *testing.T) {
		var rNode yaml.Node
		_ = yaml.Unmarshal([]byte(`newvalue`), &rNode)

		changes := []*Change{}
		CheckPropertyAdditionOrRemoval[string](nil, rNode.Content[0], "test", &changes, true, "", "new")

		assert.Len(t, changes, 1)
		assert.Equal(t, PropertyAdded, changes[0].ChangeType)
		assert.True(t, changes[0].Breaking)
	})

	t.Run("detects both removal and addition when values swap", func(t *testing.T) {
		// Simulates a property being removed from left and added to right
		// This tests that both CheckForRemoval and CheckForAddition are called
		var lNode yaml.Node
		_ = yaml.Unmarshal([]byte(`oldvalue`), &lNode)

		var rNode yaml.Node
		_ = yaml.Unmarshal([]byte(``), &rNode) // Empty value simulates removal on left side

		changes := []*Change{}
		// Left has value, right is empty - should detect removal
		CheckPropertyAdditionOrRemoval[string](lNode.Content[0], nil, "test", &changes, false, "old", "")

		assert.Len(t, changes, 1)
		assert.Equal(t, PropertyRemoved, changes[0].ChangeType)
	})

	t.Run("no changes when both nil", func(t *testing.T) {
		changes := []*Change{}
		CheckPropertyAdditionOrRemoval[string](nil, nil, "test", &changes, false, "", "")

		assert.Empty(t, changes)
	})

	t.Run("no changes when both have same value", func(t *testing.T) {
		var lNode, rNode yaml.Node
		_ = yaml.Unmarshal([]byte(`samevalue`), &lNode)
		_ = yaml.Unmarshal([]byte(`samevalue`), &rNode)

		changes := []*Change{}
		CheckPropertyAdditionOrRemoval[string](lNode.Content[0], rNode.Content[0], "test", &changes, false, "same", "same")

		assert.Empty(t, changes)
	})

	t.Run("detects removal when left has value and right node is empty", func(t *testing.T) {
		var lNode yaml.Node
		_ = yaml.Unmarshal([]byte(`hasvalue`), &lNode)

		// Create an empty node (simulating empty value)
		emptyNode := &yaml.Node{Kind: yaml.ScalarNode, Value: ""}

		changes := []*Change{}
		CheckPropertyAdditionOrRemoval[string](lNode.Content[0], emptyNode, "test", &changes, false, "old", "")

		assert.Len(t, changes, 1)
		assert.Equal(t, PropertyRemoved, changes[0].ChangeType)
	})
}

// TestCheckPropertyAdditionOrRemovalWithEncoding tests the combined addition/removal check with encoding (lines 299-305)
func TestCheckPropertyAdditionOrRemovalWithEncoding(t *testing.T) {
	t.Run("detects removal with encoding", func(t *testing.T) {
		var lNode yaml.Node
		_ = yaml.Unmarshal([]byte(`{key: value, nested: data}`), &lNode)

		changes := []*Change{}
		CheckPropertyAdditionOrRemovalWithEncoding[string](lNode.Content[0], nil, "test", &changes, true, "old", "")

		assert.Len(t, changes, 1)
		assert.Equal(t, PropertyRemoved, changes[0].ChangeType)
		assert.True(t, changes[0].Breaking)
		assert.NotEmpty(t, changes[0].OriginalEncoded)
	})

	t.Run("detects addition with encoding", func(t *testing.T) {
		var rNode yaml.Node
		_ = yaml.Unmarshal([]byte(`[item1, item2, item3]`), &rNode)

		changes := []*Change{}
		CheckPropertyAdditionOrRemovalWithEncoding[string](nil, rNode.Content[0], "test", &changes, false, "", "new")

		assert.Len(t, changes, 1)
		assert.Equal(t, PropertyAdded, changes[0].ChangeType)
		assert.NotEmpty(t, changes[0].NewEncoded)
	})

	t.Run("no changes when both nil", func(t *testing.T) {
		changes := []*Change{}
		CheckPropertyAdditionOrRemovalWithEncoding[string](nil, nil, "test", &changes, false, "", "")

		assert.Empty(t, changes)
	})

	t.Run("no changes when both have same value", func(t *testing.T) {
		var lNode, rNode yaml.Node
		_ = yaml.Unmarshal([]byte(`{same: value}`), &lNode)
		_ = yaml.Unmarshal([]byte(`{same: value}`), &rNode)

		changes := []*Change{}
		CheckPropertyAdditionOrRemovalWithEncoding[string](lNode.Content[0], rNode.Content[0], "test", &changes, false, "same", "same")

		assert.Empty(t, changes)
	})

	t.Run("detects removal of complex structure with encoding via nil", func(t *testing.T) {
		var lNode yaml.Node
		_ = yaml.Unmarshal([]byte(`{complex: {nested: {deeply: value}}}`), &lNode)

		changes := []*Change{}
		// Removal is detected when right is nil
		CheckPropertyAdditionOrRemovalWithEncoding[string](lNode.Content[0], nil, "test", &changes, true, "old", "")

		assert.Len(t, changes, 1)
		assert.Equal(t, PropertyRemoved, changes[0].ChangeType)
		assert.NotEmpty(t, changes[0].OriginalEncoded)
	})

	t.Run("detects addition of array with encoding via nil", func(t *testing.T) {
		var rNode yaml.Node
		_ = yaml.Unmarshal([]byte(`[a, b, c, d]`), &rNode)

		changes := []*Change{}
		// Addition is detected when left is nil
		CheckPropertyAdditionOrRemovalWithEncoding[string](nil, rNode.Content[0], "test", &changes, false, "", "new")

		assert.Len(t, changes, 1)
		assert.Equal(t, PropertyAdded, changes[0].ChangeType)
		assert.NotEmpty(t, changes[0].NewEncoded)
	})
}
