package lsproto

import (
	"reflect"
	"strings"
	"testing"

	"github.com/microsoft/typescript-go/internal/json"
	"gotest.tools/v3/assert"
)

func TestUnmarshalRejectsNullForOptionalNonNullableFields(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name    string
		input   string
		target  any
		errText string
	}{
		{
			name:    "InlayHint kind null",
			input:   `{"position": {"line": 0, "character": 0}, "label": "foo", "kind": null}`,
			target:  new(InlayHint),
			errText: `null value is not allowed for field "kind"`,
		},
		{
			name:    "InlayHint textEdits null",
			input:   `{"position": {"line": 0, "character": 0}, "label": "foo", "textEdits": null}`,
			target:  new(InlayHint),
			errText: `null value is not allowed for field "textEdits"`,
		},
		{
			name:    "InlayHint paddingLeft null",
			input:   `{"position": {"line": 0, "character": 0}, "label": "foo", "paddingLeft": null}`,
			target:  new(InlayHint),
			errText: `null value is not allowed for field "paddingLeft"`,
		},
		{
			name:    "FoldingRange kind null",
			input:   `{"startLine": 0, "endLine": 10, "kind": null}`,
			target:  new(FoldingRange),
			errText: `null value is not allowed for field "kind"`,
		},
		{
			name:    "FoldingRange startCharacter null",
			input:   `{"startLine": 0, "endLine": 10, "startCharacter": null}`,
			target:  new(FoldingRange),
			errText: `null value is not allowed for field "startCharacter"`,
		},
		{
			name:    "CompletionItem insertTextFormat null",
			input:   `{"label": "test", "insertTextFormat": null}`,
			target:  new(CompletionItem),
			errText: `null value is not allowed for field "insertTextFormat"`,
		},
		{
			name:    "Hover range null",
			input:   `{"contents": {"kind": "plaintext", "value": "hi"}, "range": null}`,
			target:  new(Hover),
			errText: `null value is not allowed for field "range"`,
		},
		{
			name:    "WorkDoneProgressOptions workDoneProgress null",
			input:   `{"workDoneProgress": null}`,
			target:  new(WorkDoneProgressOptions),
			errText: `null value is not allowed for field "workDoneProgress"`,
		},
		{
			name:    "CallHierarchyIncomingCallsParams item null",
			input:   `{"item": null}`,
			target:  new(CallHierarchyIncomingCallsParams),
			errText: `null value is not allowed for field "item"`,
		},
		{
			name:    "CallHierarchyIncomingCall from null",
			input:   `{"from": null, "fromRanges": []}`,
			target:  new(CallHierarchyIncomingCall),
			errText: `null value is not allowed for field "from"`,
		},
		{
			name:    "InitializeParams capabilities null",
			input:   `{"processId": null, "rootUri": null, "capabilities": null}`,
			target:  new(InitializeParams),
			errText: `null value is not allowed for field "capabilities"`,
		},
		{
			name:    "InitializeResult capabilities null",
			input:   `{"capabilities": null}`,
			target:  new(InitializeResult),
			errText: `null value is not allowed for field "capabilities"`,
		},
		{
			name:    "SemanticTokens data null (required slice)",
			input:   `{"data": null}`,
			target:  new(SemanticTokens),
			errText: `null value is not allowed for field "data"`,
		},
		{
			name:    "TextDocumentEdit edits null (required slice)",
			input:   `{"textDocument": {"uri": "file:///a.ts", "version": 1}, "edits": null}`,
			target:  new(TextDocumentEdit),
			errText: `null value is not allowed for field "edits"`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			err := json.Unmarshal([]byte(tt.input), tt.target)
			assert.ErrorContains(t, err, tt.errText)
		})
	}
}

func TestUnmarshalAcceptsNullForNullableFields(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name   string
		input  string
		target any
	}{
		{
			name:   "InitializeParams rootUri null",
			input:  `{"processId": null, "rootUri": null, "capabilities": {}}`,
			target: new(InitializeParams),
		},
		{
			name:   "InitializeParams workspaceFolders null",
			input:  `{"processId": null, "rootUri": null, "capabilities": {}, "workspaceFolders": null}`,
			target: new(InitializeParams),
		},
		{
			name:   "InitializeParams processId null",
			input:  `{"processId": null, "rootUri": null, "capabilities": {}}`,
			target: new(InitializeParams),
		},
		{
			name:   "InitializationOptions userPreferences null",
			input:  `{"userPreferences": null}`,
			target: new(InitializationOptions),
		},
		{
			name:   "InitializeParams initializationOptions null",
			input:  `{"processId": null, "rootUri": null, "capabilities": {}, "initializationOptions": null}`,
			target: new(InitializeParams),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			err := json.Unmarshal([]byte(tt.input), tt.target)
			assert.NilError(t, err)
		})
	}
}

func TestUnmarshalAcceptsOmittedOptionalFields(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name   string
		input  string
		target any
		check  func(t *testing.T, target any)
	}{
		{
			name:   "InlayHint with only required fields",
			input:  `{"position": {"line": 1, "character": 5}, "label": "test"}`,
			target: new(InlayHint),
			check: func(t *testing.T, target any) {
				t.Helper()
				hint := target.(*InlayHint)
				assert.Assert(t, hint.Kind == nil)
				assert.Assert(t, hint.TextEdits == nil)
				assert.Assert(t, hint.Tooltip == nil)
				assert.Assert(t, hint.PaddingLeft == nil)
				assert.Assert(t, hint.PaddingRight == nil)
				assert.Assert(t, hint.Data == nil)
				assert.Equal(t, hint.Position.Line, uint32(1))
				assert.Equal(t, hint.Position.Character, uint32(5))
			},
		},
		{
			name:   "FoldingRange with only required fields",
			input:  `{"startLine": 5, "endLine": 10}`,
			target: new(FoldingRange),
			check: func(t *testing.T, target any) {
				t.Helper()
				fr := target.(*FoldingRange)
				assert.Assert(t, fr.Kind == nil)
				assert.Assert(t, fr.StartCharacter == nil)
				assert.Assert(t, fr.EndCharacter == nil)
				assert.Assert(t, fr.CollapsedText == nil)
				assert.Equal(t, fr.StartLine, uint32(5))
				assert.Equal(t, fr.EndLine, uint32(10))
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			err := json.Unmarshal([]byte(tt.input), tt.target)
			assert.NilError(t, err)
			tt.check(t, tt.target)
		})
	}
}

func TestUnmarshalRejectsIncompleteObjects(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name    string
		input   string
		target  any
		errText string
	}{
		{
			name:    "InlayHint missing position",
			input:   `{"label": "test"}`,
			target:  new(InlayHint),
			errText: "missing required properties: position",
		},
		{
			name:    "InlayHint missing label",
			input:   `{"position": {"line": 0, "character": 0}}`,
			target:  new(InlayHint),
			errText: "missing required properties: label",
		},
		{
			name:    "Location missing uri",
			input:   `{"range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}}}`,
			target:  new(Location),
			errText: "missing required properties: uri",
		},
		{
			name:    "Location empty object",
			input:   `{}`,
			target:  new(Location),
			errText: "missing required properties: uri, range",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			err := json.Unmarshal([]byte(tt.input), tt.target)
			assert.ErrorContains(t, err, tt.errText)
		})
	}
}

func TestMarshalUnmarshalRoundTrip(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name  string
		value any
	}{
		{
			name: "InlayHint with kind",
			value: &InlayHint{
				Position: Position{Line: 1, Character: 5},
				Label:    StringOrInlayHintLabelParts{String: new("param")},
				Kind:     new(InlayHintKindParameter),
			},
		},
		{
			name: "InlayHint minimal",
			value: &InlayHint{
				Position: Position{Line: 0, Character: 0},
				Label:    StringOrInlayHintLabelParts{String: new("x")},
			},
		},
		{
			name: "FoldingRange with all fields",
			value: &FoldingRange{
				StartLine:      1,
				StartCharacter: new(uint32(0)),
				EndLine:        10,
				EndCharacter:   new(uint32(5)),
				Kind:           new(FoldingRangeKindRegion),
				CollapsedText:  new("..."),
			},
		},
		{
			name: "Location",
			value: &Location{
				Uri: "file:///test.ts",
				Range: Range{
					Start: Position{Line: 1, Character: 2},
					End:   Position{Line: 3, Character: 4},
				},
			},
		},
		{
			name: "InitializeParams with null processId",
			value: &InitializeParams{
				ProcessId:    IntegerOrNull{},
				RootUri:      DocumentUriOrNull{DocumentUri: new(DocumentUri("file:///workspace"))},
				Capabilities: &ClientCapabilities{},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			data, err := json.Marshal(tt.value)
			assert.NilError(t, err)

			// Unmarshal into a new value of the same type
			switch v := tt.value.(type) {
			case *InlayHint:
				var result InlayHint
				err = json.Unmarshal(data, &result)
				assert.NilError(t, err)
				assert.DeepEqual(t, *v, result)
			case *FoldingRange:
				var result FoldingRange
				err = json.Unmarshal(data, &result)
				assert.NilError(t, err)
				assert.DeepEqual(t, *v, result)
			case *Location:
				var result Location
				err = json.Unmarshal(data, &result)
				assert.NilError(t, err)
				assert.DeepEqual(t, *v, result)
			case *InitializeParams:
				var result InitializeParams
				err = json.Unmarshal(data, &result)
				assert.NilError(t, err)
				assert.DeepEqual(t, *v, result)
			default:
				t.Fatalf("unhandled type %T", tt.value)
			}
		})
	}
}

func TestUnmarshalUnionTypes(t *testing.T) {
	t.Parallel()

	t.Run("IntegerOrString with integer", func(t *testing.T) {
		t.Parallel()
		var v IntegerOrString
		err := json.Unmarshal([]byte(`42`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.Integer != nil)
		assert.Equal(t, *v.Integer, int32(42))
		assert.Assert(t, v.String == nil)
	})

	t.Run("IntegerOrString with string", func(t *testing.T) {
		t.Parallel()
		var v IntegerOrString
		err := json.Unmarshal([]byte(`"hello"`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.String != nil)
		assert.Equal(t, *v.String, "hello")
		assert.Assert(t, v.Integer == nil)
	})

	t.Run("IntegerOrNull with integer", func(t *testing.T) {
		t.Parallel()
		var v IntegerOrNull
		err := json.Unmarshal([]byte(`42`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.Integer != nil)
		assert.Equal(t, *v.Integer, int32(42))
	})

	t.Run("IntegerOrNull with null", func(t *testing.T) {
		t.Parallel()
		var v IntegerOrNull
		err := json.Unmarshal([]byte(`null`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.Integer == nil)
	})

	t.Run("DocumentUriOrNull with string", func(t *testing.T) {
		t.Parallel()
		var v DocumentUriOrNull
		err := json.Unmarshal([]byte(`"file:///test.ts"`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.DocumentUri != nil)
		assert.Equal(t, *v.DocumentUri, DocumentUri("file:///test.ts"))
	})

	t.Run("DocumentUriOrNull with null", func(t *testing.T) {
		t.Parallel()
		var v DocumentUriOrNull
		err := json.Unmarshal([]byte(`null`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.DocumentUri == nil)
	})
}

func TestMarshalUnionTypes(t *testing.T) {
	t.Parallel()

	t.Run("IntegerOrNull with value", func(t *testing.T) {
		t.Parallel()
		v := IntegerOrNull{Integer: new(int32(42))}
		data, err := json.Marshal(&v)
		assert.NilError(t, err)
		assert.Equal(t, string(data), "42")
	})

	t.Run("IntegerOrNull with null", func(t *testing.T) {
		t.Parallel()
		v := IntegerOrNull{}
		data, err := json.Marshal(&v)
		assert.NilError(t, err)
		assert.Equal(t, string(data), "null")
	})

	t.Run("IntegerOrString with integer", func(t *testing.T) {
		t.Parallel()
		v := IntegerOrString{Integer: new(int32(7))}
		data, err := json.Marshal(&v)
		assert.NilError(t, err)
		assert.Equal(t, string(data), "7")
	})

	t.Run("IntegerOrString with string", func(t *testing.T) {
		t.Parallel()
		v := IntegerOrString{String: new("tok")}
		data, err := json.Marshal(&v)
		assert.NilError(t, err)
		assert.Equal(t, string(data), `"tok"`)
	})
}

func TestUnmarshalIgnoresUnknownFields(t *testing.T) {
	t.Parallel()

	t.Run("Location with extra fields", func(t *testing.T) {
		t.Parallel()
		var loc Location
		err := json.Unmarshal([]byte(`{
			"uri": "file:///test.ts",
			"range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 5}},
			"someUnknownField": 42,
			"anotherUnknown": {"nested": true}
		}`), &loc)
		assert.NilError(t, err)
		assert.Equal(t, loc.Uri, DocumentUri("file:///test.ts"))
	})

	t.Run("InlayHint with extra fields", func(t *testing.T) {
		t.Parallel()
		var hint InlayHint
		err := json.Unmarshal([]byte(`{
			"position": {"line": 0, "character": 0},
			"label": "x",
			"futureField": [1, 2, 3]
		}`), &hint)
		assert.NilError(t, err)
	})
}

func TestUnmarshalRejectsWrongTypes(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name   string
		input  string
		target any
	}{
		{
			name:   "Location receives array",
			input:  `[]`,
			target: new(Location),
		},
		{
			name:   "Location receives string",
			input:  `"not an object"`,
			target: new(Location),
		},
		{
			name:   "Location receives number",
			input:  `42`,
			target: new(Location),
		},
		{
			name:   "Location receives null",
			input:  `null`,
			target: new(Location),
		},
		{
			name:   "FoldingRange receives boolean",
			input:  `true`,
			target: new(FoldingRange),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			err := json.Unmarshal([]byte(tt.input), tt.target)
			assert.Assert(t, err != nil, "expected error for input %s", tt.input)
		})
	}
}

func TestUnmarshalUnionTypeWrongKind(t *testing.T) {
	t.Parallel()

	t.Run("IntegerOrString rejects boolean", func(t *testing.T) {
		t.Parallel()
		var v IntegerOrString
		err := json.Unmarshal([]byte(`true`), &v)
		assert.Assert(t, err != nil)
	})

	t.Run("IntegerOrString rejects null", func(t *testing.T) {
		t.Parallel()
		var v IntegerOrString
		err := json.Unmarshal([]byte(`null`), &v)
		assert.Assert(t, err != nil)
	})

	t.Run("IntegerOrString rejects object", func(t *testing.T) {
		t.Parallel()
		var v IntegerOrString
		err := json.Unmarshal([]byte(`{}`), &v)
		assert.Assert(t, err != nil)
	})

	t.Run("IntegerOrString rejects array", func(t *testing.T) {
		t.Parallel()
		var v IntegerOrString
		err := json.Unmarshal([]byte(`[]`), &v)
		assert.Assert(t, err != nil)
	})

	t.Run("StringOrInlayHintLabelParts rejects number", func(t *testing.T) {
		t.Parallel()
		var v StringOrInlayHintLabelParts
		err := json.Unmarshal([]byte(`42`), &v)
		assert.Assert(t, err != nil)
	})

	t.Run("StringOrInlayHintLabelParts rejects boolean", func(t *testing.T) {
		t.Parallel()
		var v StringOrInlayHintLabelParts
		err := json.Unmarshal([]byte(`true`), &v)
		assert.Assert(t, err != nil)
	})
}

func TestUnmarshalBooleanUnionTypes(t *testing.T) {
	t.Parallel()

	t.Run("BooleanOrHoverOptions with true", func(t *testing.T) {
		t.Parallel()
		var v BooleanOrHoverOptions
		err := json.Unmarshal([]byte(`true`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.Boolean != nil)
		assert.Equal(t, *v.Boolean, true)
		assert.Assert(t, v.HoverOptions == nil)
	})

	t.Run("BooleanOrHoverOptions with false", func(t *testing.T) {
		t.Parallel()
		var v BooleanOrHoverOptions
		err := json.Unmarshal([]byte(`false`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.Boolean != nil)
		assert.Equal(t, *v.Boolean, false)
		assert.Assert(t, v.HoverOptions == nil)
	})

	t.Run("BooleanOrHoverOptions with object", func(t *testing.T) {
		t.Parallel()
		var v BooleanOrHoverOptions
		err := json.Unmarshal([]byte(`{}`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.Boolean == nil)
		assert.Assert(t, v.HoverOptions != nil)
	})

	t.Run("BooleanOrHoverOptions rejects string", func(t *testing.T) {
		t.Parallel()
		var v BooleanOrHoverOptions
		err := json.Unmarshal([]byte(`"nope"`), &v)
		assert.Assert(t, err != nil)
	})
}

func TestUnmarshalDiscriminatorUnion(t *testing.T) {
	t.Parallel()

	t.Run("WorkDoneProgressBegin", func(t *testing.T) {
		t.Parallel()
		var v WorkDoneProgressBeginOrReportOrEnd
		err := json.Unmarshal([]byte(`{"kind": "begin", "title": "Indexing"}`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.Begin != nil)
		assert.Assert(t, v.Report == nil)
		assert.Assert(t, v.End == nil)
		assert.Equal(t, v.Begin.Title, "Indexing")
	})

	t.Run("WorkDoneProgressReport", func(t *testing.T) {
		t.Parallel()
		var v WorkDoneProgressBeginOrReportOrEnd
		err := json.Unmarshal([]byte(`{"kind": "report", "message": "50%"}`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.Begin == nil)
		assert.Assert(t, v.Report != nil)
		assert.Assert(t, v.End == nil)
		assert.Assert(t, v.Report.Message != nil)
		assert.Equal(t, *v.Report.Message, "50%")
	})

	t.Run("WorkDoneProgressEnd", func(t *testing.T) {
		t.Parallel()
		var v WorkDoneProgressBeginOrReportOrEnd
		err := json.Unmarshal([]byte(`{"kind": "end"}`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.Begin == nil)
		assert.Assert(t, v.Report == nil)
		assert.Assert(t, v.End != nil)
	})

	t.Run("invalid discriminator", func(t *testing.T) {
		t.Parallel()
		var v WorkDoneProgressBeginOrReportOrEnd
		err := json.Unmarshal([]byte(`{"kind": "invalid"}`), &v)
		assert.Assert(t, err != nil)
	})
}

func TestUnmarshalPresenceDiscriminatorUnion(t *testing.T) {
	t.Parallel()

	t.Run("TextEdit via range field", func(t *testing.T) {
		t.Parallel()
		var v TextEditOrInsertReplaceEdit
		err := json.Unmarshal([]byte(`{
			"range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 1}},
			"newText": "x"
		}`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.TextEdit != nil)
		assert.Assert(t, v.InsertReplaceEdit == nil)
		assert.Equal(t, v.TextEdit.NewText, "x")
	})

	t.Run("InsertReplaceEdit via insert field", func(t *testing.T) {
		t.Parallel()
		var v TextEditOrInsertReplaceEdit
		err := json.Unmarshal([]byte(`{
			"insert": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 1}},
			"replace": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 2}},
			"newText": "y"
		}`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.TextEdit == nil)
		assert.Assert(t, v.InsertReplaceEdit != nil)
		assert.Equal(t, v.InsertReplaceEdit.NewText, "y")
	})
}

func TestUnmarshalStringOrArrayUnion(t *testing.T) {
	t.Parallel()

	t.Run("StringOrInlayHintLabelParts with string", func(t *testing.T) {
		t.Parallel()
		var v StringOrInlayHintLabelParts
		err := json.Unmarshal([]byte(`"hello"`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.String != nil)
		assert.Equal(t, *v.String, "hello")
		assert.Assert(t, v.InlayHintLabelParts == nil)
	})

	t.Run("StringOrInlayHintLabelParts with array", func(t *testing.T) {
		t.Parallel()
		var v StringOrInlayHintLabelParts
		err := json.Unmarshal([]byte(`[{"value": "param"}, {"value": ": "}, {"value": "string"}]`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.String == nil)
		assert.Assert(t, v.InlayHintLabelParts != nil)
		assert.Equal(t, len(*v.InlayHintLabelParts), 3)
		assert.Equal(t, (*v.InlayHintLabelParts)[0].Value, "param")
	})
}

func TestUnmarshalDocumentEditUnion(t *testing.T) {
	t.Parallel()

	t.Run("TextDocumentEdit without kind", func(t *testing.T) {
		t.Parallel()
		var v TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile
		err := json.Unmarshal([]byte(`{
			"textDocument": {"uri": "file:///a.ts", "version": 1},
			"edits": [{"range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}}, "newText": "x"}]
		}`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.TextDocumentEdit != nil)
		assert.Assert(t, v.CreateFile == nil)
		assert.Assert(t, v.RenameFile == nil)
		assert.Assert(t, v.DeleteFile == nil)
	})

	t.Run("CreateFile with kind create", func(t *testing.T) {
		t.Parallel()
		var v TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile
		err := json.Unmarshal([]byte(`{"kind": "create", "uri": "file:///new.ts"}`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.TextDocumentEdit == nil)
		assert.Assert(t, v.CreateFile != nil)
		assert.Equal(t, v.CreateFile.Uri, DocumentUri("file:///new.ts"))
	})

	t.Run("RenameFile with kind rename", func(t *testing.T) {
		t.Parallel()
		var v TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile
		err := json.Unmarshal([]byte(`{"kind": "rename", "oldUri": "file:///old.ts", "newUri": "file:///new.ts"}`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.RenameFile != nil)
		assert.Equal(t, v.RenameFile.OldUri, DocumentUri("file:///old.ts"))
	})

	t.Run("DeleteFile with kind delete", func(t *testing.T) {
		t.Parallel()
		var v TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile
		err := json.Unmarshal([]byte(`{"kind": "delete", "uri": "file:///gone.ts"}`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.DeleteFile != nil)
		assert.Equal(t, v.DeleteFile.Uri, DocumentUri("file:///gone.ts"))
	})
}

func TestUnmarshalFieldOrdering(t *testing.T) {
	t.Parallel()

	t.Run("Location with reversed field order", func(t *testing.T) {
		t.Parallel()
		var loc Location
		err := json.Unmarshal([]byte(`{
			"range": {"start": {"line": 1, "character": 2}, "end": {"line": 3, "character": 4}},
			"uri": "file:///test.ts"
		}`), &loc)
		assert.NilError(t, err)
		assert.Equal(t, loc.Uri, DocumentUri("file:///test.ts"))
		assert.Equal(t, loc.Range.Start.Line, uint32(1))
	})

	t.Run("InlayHint with kind before label", func(t *testing.T) {
		t.Parallel()
		var hint InlayHint
		err := json.Unmarshal([]byte(`{
			"kind": 1,
			"label": "x",
			"position": {"line": 0, "character": 0}
		}`), &hint)
		assert.NilError(t, err)
		assert.Assert(t, hint.Kind != nil)
		assert.Equal(t, *hint.Kind, InlayHintKindType)
	})
}

func TestUnmarshalEmptyObject(t *testing.T) {
	t.Parallel()

	t.Run("WorkDoneProgressOptions empty", func(t *testing.T) {
		t.Parallel()
		var v WorkDoneProgressOptions
		err := json.Unmarshal([]byte(`{}`), &v)
		assert.NilError(t, err)
		assert.Assert(t, v.WorkDoneProgress == nil)
	})

	t.Run("InitializationOptions empty", func(t *testing.T) {
		t.Parallel()
		var v InitializationOptions
		err := json.Unmarshal([]byte(`{}`), &v)
		assert.NilError(t, err)
	})

	t.Run("ClientCapabilities empty", func(t *testing.T) {
		t.Parallel()
		var v ClientCapabilities
		err := json.Unmarshal([]byte(`{}`), &v)
		assert.NilError(t, err)
	})

	t.Run("ServerCapabilities empty", func(t *testing.T) {
		t.Parallel()
		var v ServerCapabilities
		err := json.Unmarshal([]byte(`{}`), &v)
		assert.NilError(t, err)
	})
}

func TestMarshalOmitsZeroOptionalFields(t *testing.T) {
	t.Parallel()

	t.Run("InlayHint omits nil fields", func(t *testing.T) {
		t.Parallel()
		hint := InlayHint{
			Position: Position{Line: 0, Character: 0},
			Label:    StringOrInlayHintLabelParts{String: new("x")},
		}
		data, err := json.Marshal(&hint)
		assert.NilError(t, err)
		s := string(data)
		assert.Assert(t, !strings.Contains(s, "kind"), "should not contain 'kind', got: %s", s)
		assert.Assert(t, !strings.Contains(s, "textEdits"), "should not contain 'textEdits', got: %s", s)
		assert.Assert(t, !strings.Contains(s, "paddingLeft"), "should not contain 'paddingLeft', got: %s", s)
		assert.Assert(t, strings.Contains(s, "position"), "should contain 'position', got: %s", s)
		assert.Assert(t, strings.Contains(s, "label"), "should contain 'label', got: %s", s)
	})

	t.Run("FoldingRange omits nil optional fields", func(t *testing.T) {
		t.Parallel()
		fr := FoldingRange{StartLine: 1, EndLine: 10}
		data, err := json.Marshal(&fr)
		assert.NilError(t, err)
		s := string(data)
		assert.Assert(t, !strings.Contains(s, "kind"), "should not contain 'kind', got: %s", s)
		assert.Assert(t, !strings.Contains(s, "startCharacter"), "should not contain 'startCharacter', got: %s", s)
		assert.Assert(t, strings.Contains(s, "startLine"), "should contain 'startLine', got: %s", s)
		assert.Assert(t, strings.Contains(s, "endLine"), "should contain 'endLine', got: %s", s)
	})
}

func TestLiteralTypes(t *testing.T) {
	t.Parallel()

	t.Run("StringLiteralCreate marshal", func(t *testing.T) {
		t.Parallel()
		v := StringLiteralCreate{}
		data, err := json.Marshal(v)
		assert.NilError(t, err)
		assert.Equal(t, string(data), `"create"`)
	})

	t.Run("StringLiteralCreate unmarshal", func(t *testing.T) {
		t.Parallel()
		var v StringLiteralCreate
		err := json.Unmarshal([]byte(`"create"`), &v)
		assert.NilError(t, err)
	})

	t.Run("StringLiteralCreate rejects wrong value", func(t *testing.T) {
		t.Parallel()
		var v StringLiteralCreate
		err := json.Unmarshal([]byte(`"delete"`), &v)
		assert.Assert(t, err != nil)
	})

	t.Run("StringLiteralCreate rejects wrong type", func(t *testing.T) {
		t.Parallel()
		var v StringLiteralCreate
		err := json.Unmarshal([]byte(`42`), &v)
		assert.Assert(t, err != nil)
	})
}

func TestEnumStringValues(t *testing.T) {
	t.Parallel()

	t.Run("InlayHintKind values", func(t *testing.T) {
		t.Parallel()
		assert.Equal(t, InlayHintKindType.String(), "Type")
		assert.Equal(t, InlayHintKindParameter.String(), "Parameter")
	})

	t.Run("SymbolKind values", func(t *testing.T) {
		t.Parallel()
		assert.Equal(t, SymbolKindFile.String(), "File")
		assert.Equal(t, SymbolKindFunction.String(), "Function")
		assert.Equal(t, SymbolKindVariable.String(), "Variable")
	})

	t.Run("unknown enum value", func(t *testing.T) {
		t.Parallel()
		v := InlayHintKind(999)
		s := v.String()
		assert.Assert(t, strings.Contains(s, "999"), "should contain the numeric value, got: %s", s)
	})
}

// TestRoundTrip locks the generated codecs: every value must survive
// marshal -> unmarshal unchanged. This guards fidelity so codec changes
// (e.g. pruning or table-driving them) cannot silently corrupt the wire
// format. Cover a representative spread of shapes: required fields,
// nullable/non-nullable optionals, enums, slices, nested objects, and
// unions.
func TestRoundTrip(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name  string
		value any
	}{
		{"Range", &Range{
			Start: Position{Line: 1, Character: 2},
			End:   Position{Line: 3, Character: 4},
		}},
		{"TextEdit", &TextEdit{
			Range:   Range{Start: Position{Line: 1, Character: 2}, End: Position{Line: 3, Character: 4}},
			NewText: "hello",
		}},
		{"MarkupContent", &MarkupContent{Kind: MarkupKindMarkdown, Value: "**x**"}},
		{"DidChangeConfigurationParams object", &DidChangeConfigurationParams{
			Settings: map[string]any{"js/ts": map[string]any{"x": float64(1)}},
		}},
		{"DidChangeConfigurationParams null", &DidChangeConfigurationParams{Settings: nil}},
		{"CompletionItem", &CompletionItem{
			Label:            "pageXOffset",
			Kind:             new(CompletionItemKindField),
			SortText:         new("15"),
			InsertTextFormat: new(InsertTextFormatPlainText),
		}},
		// StringOrTuple union (string arm and tuple arm).
		{"ParameterInformation string label", &ParameterInformation{
			Label: StringOrTuple{String: new("p: number")},
		}},
		{"ParameterInformation tuple label", &ParameterInformation{
			Label: StringOrTuple{Tuple: &[2]uint32{0, 4}},
		}},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			data, err := json.Marshal(tt.value)
			assert.NilError(t, err)
			got := reflect.New(reflect.TypeOf(tt.value).Elem()).Interface()
			assert.NilError(t, json.Unmarshal(data, got))
			again, err := json.Marshal(got)
			assert.NilError(t, err)
			assert.Equal(t, string(data), string(again), "re-marshal differs")
		})
	}
}

// TestStrictnessMissingRequired confirms required fields are still enforced;
// default reflective decoding would silently accept these.
func TestStrictnessMissingRequired(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name   string
		input  string
		target any
	}{
		{"TextEdit missing newText", `{"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":1}}}`, new(TextEdit)},
		{"Range missing end", `{"start":{"line":0,"character":0}}`, new(Range)},
		{"Position missing character", `{"line":0}`, new(Position)},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			err := json.Unmarshal([]byte(tt.input), tt.target)
			assert.ErrorContains(t, err, "missing required properties")
		})
	}
}

// TestStrictnessNotObject confirms a non-object where an object is required
// is rejected rather than coerced.
func TestStrictnessNotObject(t *testing.T) {
	t.Parallel()
	err := json.Unmarshal([]byte(`"oops"`), new(TextEdit))
	assert.Assert(t, err != nil)
	assert.Assert(t, strings.Contains(err.Error(), "object") || strings.Contains(err.Error(), "cannot unmarshal"))
}

// TestUnmarshalParamsRequiresParams verifies that a NoParams method must be
// given no params while every other method must be given params, and that a
// mismatch (including a null value either way) is an InvalidParams error.
func TestUnmarshalParamsRequiresParams(t *testing.T) {
	t.Parallel()

	// NoParams: only truly-absent/empty params are accepted; null and any
	// present value are rejected.
	noParamsTests := []struct {
		name    string
		params  any
		wantErr bool
	}{
		{"absent", nil, false},
		{"empty", json.Value(``), false},
		{"null", json.Value(`null`), true},
		{"object", json.Value(`{}`), true},
	}
	for _, tt := range noParamsTests {
		t.Run("NoParams/"+tt.name, func(t *testing.T) {
			t.Parallel()
			_, err := UnmarshalParams[NoParams](&RequestMessage{Params: tt.params})
			if tt.wantErr {
				assert.ErrorIs(t, err, ErrorCodeInvalidParams)
			} else {
				assert.NilError(t, err)
			}
		})
	}

	// Required-params method: only an object or array is accepted; absent,
	// empty, null, and other scalars are rejected.
	typedTests := []struct {
		name    string
		params  any
		wantErr bool
	}{
		{"absent", nil, true},
		{"empty", json.Value(``), true},
		{"null", json.Value(`null`), true},
		{"number", json.Value(`5`), true},
		{"string", json.Value(`"x"`), true},
		{"object", json.Value(`{"settings":{"x":1}}`), false},
	}
	for _, tt := range typedTests {
		t.Run("typed/"+tt.name, func(t *testing.T) {
			t.Parallel()
			got, err := UnmarshalParams[*DidChangeConfigurationParams](&RequestMessage{Params: tt.params})
			if tt.wantErr {
				assert.ErrorIs(t, err, ErrorCodeInvalidParams)
			} else {
				assert.NilError(t, err)
				assert.Assert(t, got != nil && got.Settings != nil)
			}
		})
	}
}
