package lsp_test

import (
	"context"
	"io"
	"strings"
	"testing"

	"github.com/microsoft/typescript-go/internal/bundled"
	"github.com/microsoft/typescript-go/internal/lsp"
	"github.com/microsoft/typescript-go/internal/lsp/lsproto"
	"github.com/microsoft/typescript-go/internal/testutil/lsptestutil"
	"github.com/microsoft/typescript-go/internal/vfs/vfstest"
	"gotest.tools/v3/assert"
)

// TestSemanticTokensCRLF reproduces a crash where semantic tokens panics with
// "token spans multiple lines" when the editor opens a file with CRLF line endings
// but the project originally loaded the file from disk with LF line endings.
//
// The SourceFile AST keeps positions from the LF text, but the converter's
// line map is recomputed from the CRLF overlay, causing a mismatch.
func TestSemanticTokensCRLF(t *testing.T) {
	t.Parallel()
	if !bundled.Embedded {
		t.Skip("bundled files are not embedded")
	}

	// Enough lines so the cumulative \r\n vs \n offset difference
	// causes an LF-based position to land on a \r in the CRLF text.
	fileOnDisk := "var x\nvar x\nvar x\nvar x\nvar x\nvar x\nconst a = 1\n"
	fileFromEditor := strings.ReplaceAll(fileOnDisk, "\n", "\r\n")

	files := map[string]string{
		"/home/projects/tsconfig.json": `{}`,
		"/home/projects/test.ts":       fileOnDisk,
		"/home/projects/other.ts":      "export {}",
	}
	fs := bundled.WrapFS(vfstest.FromMap(files, false))

	onServerRequest := func(_ context.Context, req *lsproto.RequestMessage) *lsproto.ResponseMessage {
		if req.Method == lsproto.MethodClientRegisterCapability || req.Method == lsproto.MethodClientUnregisterCapability {
			return &lsproto.ResponseMessage{ID: req.ID, JSONRPC: req.JSONRPC, Result: lsproto.Null{}}
		}
		return nil
	}

	client, closeClient := lsptestutil.NewLSPClient(t, lsp.ServerOptions{
		Err: io.Discard, Cwd: "/home/projects", FS: fs, DefaultLibraryPath: bundled.LibPath(),
	}, onServerRequest)
	t.Cleanup(func() { _ = closeClient() })

	initMsg, _, ok := lsptestutil.SendRequest(t, client, lsproto.InitializeInfo, &lsproto.InitializeParams{
		Capabilities: &lsproto.ClientCapabilities{
			TextDocument: &lsproto.TextDocumentClientCapabilities{
				SemanticTokens: &lsproto.SemanticTokensClientCapabilities{
					Requests: &lsproto.ClientSemanticTokensRequestOptions{
						Full: &lsproto.BooleanOrClientSemanticTokensRequestFullDelta{Boolean: new(true)},
					},
					TokenTypes:     []string{"namespace", "type", "class", "enum", "interface", "struct", "typeParameter", "parameter", "variable", "property", "enumMember", "event", "function", "method", "macro", "keyword", "modifier", "comment", "string", "number", "regexp", "operator", "decorator"},
					TokenModifiers: []string{"declaration", "definition", "readonly", "static", "deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary", "local"},
				},
			},
		},
	})
	assert.Assert(t, ok && initMsg.AsResponse().Error == nil, "Initialize failed")
	lsptestutil.SendNotification(t, client, lsproto.InitializedInfo, &lsproto.InitializedParams{})
	<-client.Server.InitComplete()

	// Open another project file to force the project to load test.ts from disk (LF).
	otherUri := lsproto.DocumentUri("file:///home/projects/other.ts")
	lsptestutil.SendNotification(t, client, lsproto.TextDocumentDidOpenInfo, &lsproto.DidOpenTextDocumentParams{
		TextDocument: &lsproto.TextDocumentItem{Uri: otherUri, LanguageId: "typescript", Text: files["/home/projects/other.ts"]},
	})
	msg1, _, _ := lsptestutil.SendRequest(t, client, lsproto.TextDocumentSemanticTokensFullInfo, &lsproto.SemanticTokensParams{
		TextDocument: lsproto.TextDocumentIdentifier{Uri: otherUri},
	})
	assert.Assert(t, msg1.AsResponse().Error == nil, "Initial request failed")

	// Open test.ts with CRLF content; the project already parsed it from disk (LF).
	uri := lsproto.DocumentUri("file:///home/projects/test.ts")
	lsptestutil.SendNotification(t, client, lsproto.TextDocumentDidOpenInfo, &lsproto.DidOpenTextDocumentParams{
		TextDocument: &lsproto.TextDocumentItem{Uri: uri, LanguageId: "typescript", Text: fileFromEditor},
	})

	// This panics: AST positions are LF-based but the line map is CRLF-based.
	msg, _, _ := lsptestutil.SendRequest(t, client, lsproto.TextDocumentSemanticTokensFullInfo, &lsproto.SemanticTokensParams{
		TextDocument: lsproto.TextDocumentIdentifier{Uri: uri},
	})
	if msg.AsResponse().Error != nil {
		t.Fatalf("Semantic tokens request failed: %s", msg.AsResponse().Error.Message)
	}
}
