package lsutil

import (
	"reflect"
	"testing"

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

func fillNonZeroValues(v reflect.Value) {
	t := v.Type()
	for i := range t.NumField() {
		field := v.Field(i)
		if !field.CanSet() {
			continue
		}
		switch field.Kind() {
		case reflect.Bool:
			field.SetBool(true)
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			field.SetInt(1)
		case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
			field.SetUint(1)
		case reflect.String:
			val := getValidStringValue(field.Type())
			field.SetString(val)
		case reflect.Slice:
			if field.Type().Elem().Kind() == reflect.String {
				field.Set(reflect.ValueOf([]string{"test"}))
			}
		case reflect.Struct:
			fillNonZeroValues(field)
		}
	}
}

func getValidStringValue(t reflect.Type) string {
	typeName := t.String()
	switch typeName {
	case "lsutil.QuotePreference":
		return string(QuotePreferenceSingle)
	case "lsutil.JsxAttributeCompletionStyle":
		return string(JsxAttributeCompletionStyleBraces)
	case "lsutil.IncludeInlayParameterNameHints":
		return string(IncludeInlayParameterNameHintsAll)
	case "lsutil.SemicolonPreference":
		return string(SemicolonPreferenceInsert)
	case "modulespecifiers.ImportModuleSpecifierPreference":
		return string(modulespecifiers.ImportModuleSpecifierPreferenceRelative)
	case "modulespecifiers.ImportModuleSpecifierEndingPreference":
		return string(modulespecifiers.ImportModuleSpecifierEndingPreferenceJs)
	default:
		return "test"
	}
}

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

	var original UserPreferences
	fillNonZeroValues(reflect.ValueOf(&original).Elem())

	jsonBytes, err := json.Marshal(&original)
	assert.NilError(t, err)

	t.Run("UnmarshalJSONFrom", func(t *testing.T) {
		t.Parallel()
		var parsed UserPreferences
		err2 := json.Unmarshal(jsonBytes, &parsed)
		assert.NilError(t, err2)
		assert.DeepEqual(t, original, parsed)
	})

	t.Run("withConfig", func(t *testing.T) {
		t.Parallel()
		var config map[string]any
		err2 := json.Unmarshal(jsonBytes, &config)
		assert.NilError(t, err2)
		parsed := UserPreferences{}.withConfig(config)
		assert.DeepEqual(t, original, parsed)
	})
}

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

	t.Run("config path field serializes to nested path", func(t *testing.T) {
		t.Parallel()
		prefs := &UserPreferences{
			QuotePreference: QuotePreferenceSingle,
		}
		jsonBytes, err := json.Marshal(prefs)
		assert.NilError(t, err)

		var actual map[string]any
		err = json.Unmarshal(jsonBytes, &actual)
		assert.NilError(t, err)

		preferences := actual["preferences"].(map[string]any)
		assert.Equal(t, "single", preferences["quoteStyle"])
	})

	t.Run("raw-only field serializes to unstable section", func(t *testing.T) {
		t.Parallel()
		prefs := &UserPreferences{
			DisableSuggestions: core.TSTrue,
		}
		jsonBytes, err := json.Marshal(prefs)
		assert.NilError(t, err)

		var actual map[string]any
		err = json.Unmarshal(jsonBytes, &actual)
		assert.NilError(t, err)

		unstable := actual["unstable"].(map[string]any)
		assert.Equal(t, true, unstable["disableSuggestions"])
	})

	t.Run("inlay hint inversion on serialize", func(t *testing.T) {
		t.Parallel()
		prefs := &UserPreferences{
			InlayHints: InlayHintsPreferences{
				IncludeInlayParameterNameHints:                        IncludeInlayParameterNameHintsAll,
				IncludeInlayParameterNameHintsWhenArgumentMatchesName: core.TSTrue,
			},
		}
		jsonBytes, err := json.Marshal(prefs)
		assert.NilError(t, err)

		var actual map[string]any
		err = json.Unmarshal(jsonBytes, &actual)
		assert.NilError(t, err)

		inlayHints := actual["inlayHints"].(map[string]any)
		parameterNames := inlayHints["parameterNames"].(map[string]any)
		assert.Equal(t, "all", parameterNames["enabled"])
		assert.Equal(t, false, parameterNames["suppressWhenArgumentMatchesName"]) // inverted
	})

	t.Run("mixed config and unstable fields", func(t *testing.T) {
		t.Parallel()
		prefs := &UserPreferences{
			QuotePreference:      QuotePreferenceSingle,
			DisableSuggestions:   core.TSTrue,
			DisplayPartsForJSDoc: core.TSTrue,
		}
		jsonBytes, err := json.Marshal(prefs)
		assert.NilError(t, err)

		var actual map[string]any
		err = json.Unmarshal(jsonBytes, &actual)
		assert.NilError(t, err)

		preferences := actual["preferences"].(map[string]any)
		assert.Equal(t, "single", preferences["quoteStyle"])

		unstable := actual["unstable"].(map[string]any)
		assert.Equal(t, true, unstable["disableSuggestions"])
		assert.Equal(t, true, unstable["displayPartsForJSDoc"])
	})
}

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

	tests := []struct {
		name     string
		json     string
		expected UserPreferences
	}{
		{
			name: "unstable fields with correct casing",
			json: `{
				"unstable": {
					"disableSuggestions": true,
					"maximumHoverLength": 100,
					"allowRenameOfImportPath": true
				}
			}`,
			expected: UserPreferences{
				DisableSuggestions:      core.TSTrue,
				MaximumHoverLength:      100,
				AllowRenameOfImportPath: core.TSTrue,
			},
		},
		{
			name: "nested preferences path",
			json: `{
				"preferences": {
					"quoteStyle": "single",
					"useAliasesForRenames": true
				}
			}`,
			expected: UserPreferences{
				QuotePreference:     QuotePreferenceSingle,
				UseAliasesForRename: core.TSTrue,
			},
		},
		{
			name: "suggest section",
			json: `{
				"suggest": {
					"autoImports": false,
					"includeCompletionsForImportStatements": true
				}
			}`,
			expected: UserPreferences{
				IncludeCompletionsForModuleExports:    core.TSFalse,
				IncludeCompletionsForImportStatements: core.TSTrue,
			},
		},
		{
			name: "inlayHints with invert",
			json: `{
				"inlayHints": {
					"parameterNames": {
						"enabled": "all",
						"suppressWhenArgumentMatchesName": true
					}
				}
			}`,
			expected: UserPreferences{
				InlayHints: InlayHintsPreferences{
					IncludeInlayParameterNameHints:                        IncludeInlayParameterNameHintsAll,
					IncludeInlayParameterNameHintsWhenArgumentMatchesName: core.TSFalse, // inverted
				},
			},
		},
		{
			name: "mixed config",
			json: `{
				"unstable": {
					"displayPartsForJSDoc": true
				},
				"preferences": {
					"importModuleSpecifier": "relative"
				},
				"workspaceSymbols": {
					"excludeLibrarySymbols": true
				}
			}`,
			expected: UserPreferences{
				DisplayPartsForJSDoc:            core.TSTrue,
				ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceRelative,
				ExcludeLibrarySymbolsInNavTo:    core.TSTrue,
			},
		},
		{
			name: "stable config overrides unstable",
			json: `{
				"unstable": {
					"quotePreference": "double"
				},
				"preferences": {
					"quoteStyle": "single"
				}
			}`,
			expected: UserPreferences{
				QuotePreference: QuotePreferenceSingle, // stable wins
			},
		},
		{
			name: "unstable sets value when no stable config",
			json: `{
				"unstable": {
					"includeAutomaticOptionalChainCompletions": false
				}
			}`,
			expected: UserPreferences{
				IncludeAutomaticOptionalChainCompletions: core.TSFalse,
			},
		},
		{
			name: "any field can be passed via unstable by its raw name",
			json: `{
				"unstable": {
					"quotePreference": "double",
					"includeCompletionsForModuleExports": true,
					"excludeLibrarySymbolsInNavTo": true
				}
			}`,
			expected: UserPreferences{
				QuotePreference:                    QuotePreferenceDouble,
				IncludeCompletionsForModuleExports: core.TSTrue,
				ExcludeLibrarySymbolsInNavTo:       core.TSTrue,
			},
		},
		{
			name: "TypeScript raw names work in unstable section",
			json: `{
				"unstable": {
					"includeCompletionsForModuleExports": true,
					"quotePreference": "single",
					"providePrefixAndSuffixTextForRename": true,
					"includeInlayParameterNameHints": "all",
					"organizeImportsLocale": "en"
				}
			}`,
			expected: UserPreferences{
				IncludeCompletionsForModuleExports: core.TSTrue,
				QuotePreference:                    QuotePreferenceSingle,
				UseAliasesForRename:                core.TSTrue,
				OrganizeImportsLocale:              "en",
				InlayHints: InlayHintsPreferences{
					IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll,
				},
			},
		},
		{
			name: "old raw organize imports unicode preferences load as raw state",
			json: `{
				"unstable": {
					"organizeImportsCollation": "unicode",
					"organizeImportsCaseFirst": "upper",
					"organizeImportsIgnoreCase": false,
					"organizeImportsNumericCollation": true
				}
			}`,
			expected: UserPreferences{
				OrganizeImportsCollation:        OrganizeImportsCollationUnicode,
				OrganizeImportsCaseFirst:        OrganizeImportsCaseFirstUpper,
				OrganizeImportsIgnoreCase:       core.TSFalse,
				OrganizeImportsNumericCollation: core.TSTrue,
			},
		},
		{
			name: "old top-level raw organize imports unicode preferences load as raw state",
			json: `{
				"organizeImportsCollation": "unicode",
				"organizeImportsIgnoreCase": true
			}`,
			expected: UserPreferences{
				OrganizeImportsCollation:  OrganizeImportsCollationUnicode,
				OrganizeImportsIgnoreCase: core.TSTrue,
			},
		},
		{
			name: "new top-level raw organize imports sort is accepted",
			json: `{
				"organizeImportsSort": "natural"
			}`,
			expected: UserPreferences{
				OrganizeImportsSort: OrganizeImportsSortNatural,
			},
		},
		{
			name: "old raw organize imports ignore case loads as raw state",
			json: `{
				"unstable": {
					"organizeImportsIgnoreCase": true
				}
			}`,
			expected: UserPreferences{
				OrganizeImportsIgnoreCase: core.TSTrue,
			},
		},
		{
			name: "new raw organize imports sort loads alongside old raw preferences",
			json: `{
				"unstable": {
					"organizeImportsSort": "ordinal",
					"organizeImportsCollation": "unicode",
					"organizeImportsIgnoreCase": true
				}
			}`,
			expected: UserPreferences{
				OrganizeImportsSort:       OrganizeImportsSortOrdinal,
				OrganizeImportsCollation:  OrganizeImportsCollationUnicode,
				OrganizeImportsIgnoreCase: core.TSTrue,
			},
		},
		{
			name: "old nested organize imports unicode preferences load as raw state",
			json: `{
				"preferences": {
					"organizeImports": {
						"unicodeCollation": "unicode",
						"caseSensitivity": "caseSensitive",
						"numericCollation": true,
						"caseFirst": "upper"
					}
				}
			}`,
			expected: UserPreferences{
				OrganizeImportsCollation:        OrganizeImportsCollationUnicode,
				OrganizeImportsIgnoreCase:       core.TSFalse,
				OrganizeImportsNumericCollation: core.TSTrue,
				OrganizeImportsCaseFirst:        OrganizeImportsCaseFirstUpper,
			},
		},
		{
			name: "new nested organize imports sort loads alongside old nested preferences",
			json: `{
				"preferences": {
					"organizeImports": {
						"sort": "ordinalIgnoreCase",
						"unicodeCollation": "unicode",
						"caseSensitivity": "caseSensitive"
					}
				}
			}`,
			expected: UserPreferences{
				OrganizeImportsSort:       OrganizeImportsSortOrdinalIgnoreCase,
				OrganizeImportsCollation:  OrganizeImportsCollationUnicode,
				OrganizeImportsIgnoreCase: core.TSFalse,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			var config map[string]any
			err := json.Unmarshal([]byte(tt.json), &config)
			assert.NilError(t, err)

			parsed := UserPreferences{}.withConfig(config)

			assert.DeepEqual(t, tt.expected, parsed)
		})
	}
}

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

	t.Run("reportStyleChecksAsWarnings via config path", func(t *testing.T) {
		t.Parallel()
		prefs := ParseUserPreferences(map[string]any{
			"js/ts": map[string]any{
				"reportStyleChecksAsWarnings": false,
			},
		})
		assert.Equal(t, prefs.ReportStyleChecksAsWarnings, core.TSFalse)
	})

	t.Run("reportStyleChecksAsWarnings defaults to true", func(t *testing.T) {
		t.Parallel()
		prefs := NewDefaultUserPreferences()
		assert.Equal(t, prefs.ReportStyleChecksAsWarnings, core.TSTrue)
	})

	t.Run("reportStyleChecksAsWarnings via unstable section", func(t *testing.T) {
		t.Parallel()
		prefs := ParseUserPreferences(map[string]any{
			"js/ts": map[string]any{
				"unstable": map[string]any{
					"reportStyleChecksAsWarnings": false,
				},
			},
		})
		assert.Equal(t, prefs.ReportStyleChecksAsWarnings, core.TSFalse)
	})
}

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

	t.Run("preferred server feature settings", func(t *testing.T) {
		t.Parallel()
		prefs := ParseUserPreferences(map[string]any{
			"js/ts": map[string]any{
				"validate": map[string]any{"enabled": false},
				"format":   map[string]any{"enabled": false},
				"autoClosingTags": map[string]any{
					"enabled": false,
				},
			},
		})
		assert.Equal(t, prefs.EnableValidation, core.TSFalse)
		assert.Equal(t, prefs.EnableFormatting, core.TSFalse)
		assert.Equal(t, prefs.EnableAutoClosingTags, core.TSFalse)
	})

	t.Run("legacy server feature fallbacks", func(t *testing.T) {
		t.Parallel()
		prefs := ParseUserPreferences(map[string]any{
			"typescript": map[string]any{
				"validate":        map[string]any{"enable": false},
				"format":          map[string]any{"enable": false},
				"autoClosingTags": false,
			},
		})
		assert.Equal(t, prefs.EnableValidation, core.TSFalse)
		assert.Equal(t, prefs.EnableFormatting, core.TSFalse)
		assert.Equal(t, prefs.EnableAutoClosingTags, core.TSFalse)
	})

	t.Run("preferred settings take precedence over fallbacks", func(t *testing.T) {
		t.Parallel()
		prefs := ParseUserPreferences(map[string]any{
			"typescript": map[string]any{
				"validate":        map[string]any{"enable": false},
				"format":          map[string]any{"enable": false},
				"autoClosingTags": false,
			},
			"js/ts": map[string]any{
				"validate": map[string]any{"enabled": true},
				"format":   map[string]any{"enabled": true},
				"autoClosingTags": map[string]any{
					"enabled": true,
				},
			},
		})
		assert.Equal(t, prefs.EnableValidation, core.TSTrue)
		assert.Equal(t, prefs.EnableFormatting, core.TSTrue)
		assert.Equal(t, prefs.EnableAutoClosingTags, core.TSTrue)
	})
}

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

	t.Run("unified jsdoc enabled setting", func(t *testing.T) {
		t.Parallel()
		prefs := ParseUserPreferences(map[string]any{
			"js/ts": map[string]any{
				"suggest": map[string]any{
					"jsdoc": map[string]any{
						"enabled": false,
					},
				},
			},
		})
		assert.Equal(t, prefs.EnableJSDocCompletions, core.TSFalse)
	})

	t.Run("language fallback completeJSDocs setting", func(t *testing.T) {
		t.Parallel()
		prefs := ParseUserPreferences(map[string]any{
			"typescript": map[string]any{
				"suggest": map[string]any{
					"completeJSDocs": false,
				},
			},
		})
		assert.Equal(t, prefs.EnableJSDocCompletions, core.TSFalse)
	})

	t.Run("unified jsdoc enabled takes precedence over language fallback", func(t *testing.T) {
		t.Parallel()
		prefs := ParseUserPreferences(map[string]any{
			"typescript": map[string]any{
				"suggest": map[string]any{
					"completeJSDocs": false,
				},
			},
			"js/ts": map[string]any{
				"suggest": map[string]any{
					"jsdoc": map[string]any{
						"enabled": true,
					},
				},
			},
		})
		assert.Equal(t, prefs.EnableJSDocCompletions, core.TSTrue)
	})

	t.Run("unified jsdoc generateReturns setting", func(t *testing.T) {
		t.Parallel()
		prefs := ParseUserPreferences(map[string]any{
			"js/ts": map[string]any{
				"suggest": map[string]any{
					"jsdoc": map[string]any{
						"generateReturns": false,
					},
				},
			},
		})
		assert.Equal(t, prefs.GenerateReturnInDocTemplate, core.TSFalse)
	})

	t.Run("language jsdoc generateReturns setting", func(t *testing.T) {
		t.Parallel()
		prefs := ParseUserPreferences(map[string]any{
			"typescript": map[string]any{
				"suggest": map[string]any{
					"jsdoc": map[string]any{
						"generateReturns": false,
					},
				},
			},
		})
		assert.Equal(t, prefs.GenerateReturnInDocTemplate, core.TSFalse)
	})
}

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

	t.Run("ParseUserPreferences with unified ATA setting in js/ts section", func(t *testing.T) {
		t.Parallel()
		prefs := ParseUserPreferences(map[string]any{
			"js/ts": map[string]any{
				"tsserver": map[string]any{
					"automaticTypeAcquisition": map[string]any{
						"enabled": false,
					},
				},
			},
		})
		assert.Assert(t, prefs.IsATADisabled())
		assert.Equal(t, prefs.AutomaticTypeAcquisitionEnabled, core.TSFalse)
	})

	t.Run("ParseUserPreferences with deprecated disableAutomaticTypeAcquisition in typescript section", func(t *testing.T) {
		t.Parallel()
		prefs := ParseUserPreferences(map[string]any{
			"typescript": map[string]any{
				"disableAutomaticTypeAcquisition": true,
			},
		})
		assert.Assert(t, prefs.IsATADisabled())
		assert.Equal(t, prefs.DisableAutomaticTypeAcquisition, core.TSTrue)
	})

	t.Run("unified setting takes precedence over deprecated setting", func(t *testing.T) {
		t.Parallel()
		// Both settings set: unified (js/ts) should take precedence
		prefs := ParseUserPreferences(map[string]any{
			"typescript": map[string]any{
				"disableAutomaticTypeAcquisition": true,
			},
			"js/ts": map[string]any{
				"tsserver": map[string]any{
					"automaticTypeAcquisition": map[string]any{
						"enabled": true,
					},
				},
			},
		})
		assert.Assert(t, !prefs.IsATADisabled())
		assert.Equal(t, prefs.AutomaticTypeAcquisitionEnabled, core.TSTrue)
	})

	t.Run("IsATADisabled returns false when neither setting is configured", func(t *testing.T) {
		t.Parallel()
		prefs := NewDefaultUserPreferences()
		assert.Assert(t, !prefs.IsATADisabled())
	})
}
