// Copyright 2020-2026 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package buflsp_test

import (
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.lsp.dev/protocol"
)

// TestBufLockDocumentLinks verifies that document links are returned for
// deps[*].name module references in buf.lock files.
func TestBufLockDocumentLinks(t *testing.T) {
	t.Parallel()

	// Fixture layout (0-indexed lines):
	//  0: # Generated by buf. DO NOT EDIT.
	//  1: version: v2
	//  2: deps:
	//  3:   - name: buf.build/bufbuild/protovalidate
	//  4:     commit: 7a6bc1e3207144b38e9066861e1de0ff
	//  5:     digest: b5:abc123
	//  6:   - name: buf.build/googleapis/googleapis
	//  7:     commit: 2a6bc1e3207144b38e9066861e1de0aa
	//  8:     digest: b5:def456

	tests := []struct {
		name      string
		fixture   string
		wantLinks []protocol.DocumentLink
	}{
		{
			name:    "invalid",
			fixture: "testdata/buf_lock/invalid/buf.lock",
			// Malformed YAML must not crash; returns no links.
		},
		{
			name:    "with_deps",
			fixture: "testdata/buf_lock/document_link/buf.lock",
			wantLinks: []protocol.DocumentLink{
				{
					// deps[0].name: buf.build/bufbuild/protovalidate
					Range: protocol.Range{
						Start: protocol.Position{Line: 3, Character: 10},
						End:   protocol.Position{Line: 3, Character: 42},
					},
					Target: "https://buf.build/bufbuild/protovalidate",
				},
				{
					// deps[1].name: buf.build/googleapis/googleapis
					Range: protocol.Range{
						Start: protocol.Position{Line: 6, Character: 10},
						End:   protocol.Position{Line: 6, Character: 41},
					},
					Target: "https://buf.build/googleapis/googleapis",
				},
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			absPath, err := filepath.Abs(tc.fixture)
			require.NoError(t, err)

			clientJSONConn, bufLockURI, _ := setupLSPServerForBufYAML(t, absPath, nil, nil)
			ctx := t.Context()

			var links []protocol.DocumentLink
			_, err = clientJSONConn.Call(ctx, protocol.MethodTextDocumentDocumentLink, &protocol.DocumentLinkParams{
				TextDocument: protocol.TextDocumentIdentifier{URI: bufLockURI},
			}, &links)
			require.NoError(t, err)
			require.Len(t, links, len(tc.wantLinks))
			for i, want := range tc.wantLinks {
				assert.Equal(t, want.Range, links[i].Range, "link %d range", i)
				assert.Equal(t, want.Target, links[i].Target, "link %d target", i)
			}
		})
	}
}

// TestBufLockHoverMalformedYAML verifies that hovering over a buf.lock with
// invalid YAML syntax returns no hover and does not crash the server.
func TestBufLockHoverMalformedYAML(t *testing.T) {
	t.Parallel()

	absPath, err := filepath.Abs("testdata/buf_lock/invalid/buf.lock")
	require.NoError(t, err)

	clientJSONConn, bufLockURI, _ := setupLSPServerForBufYAML(t, absPath, nil, nil)
	ctx := t.Context()

	var hover *protocol.Hover
	_, err = clientJSONConn.Call(ctx, protocol.MethodTextDocumentHover, &protocol.HoverParams{
		TextDocumentPositionParams: protocol.TextDocumentPositionParams{
			TextDocument: protocol.TextDocumentIdentifier{URI: bufLockURI},
			Position:     protocol.Position{Line: 0, Character: 0},
		},
	}, &hover)
	require.NoError(t, err)
	assert.Nil(t, hover, "malformed YAML must not crash the server or return hover")
}

// TestBufLockHover verifies that hovering over buf.lock keys returns the
// correct markdown documentation.
func TestBufLockHover(t *testing.T) {
	t.Parallel()

	fixture := "testdata/buf_lock/hover/buf.lock"

	// The fixture layout (0-indexed lines):
	//  0: # Generated by buf. DO NOT EDIT.
	//  1: version: v2
	//  2: deps:
	//  3:   - name: buf.build/googleapis/googleapis
	//  4:     commit: 7a6bc1e3207144b38e9066861e1de0ff
	//  5:     digest: b5:6d05...

	tests := []struct {
		name             string
		line             uint32
		character        uint32
		expectedContains []string
		expectNoHover    bool
	}{
		{
			name: "version_key",
			line: 1, character: 0,
			expectedContains: []string{"version", "configuration format version", "https://buf.build/docs/configuration/v2/buf-lock/#version"},
		},
		{
			name: "deps_key",
			line: 2, character: 0,
			expectedContains: []string{"deps", "Pinned module dependencies", "buf dep update", "https://buf.build/docs/configuration/v2/buf-lock/#deps"},
		},
		{
			name: "dep_name_key",
			line: 3, character: 4,
			expectedContains: []string{"name", "full module identifier", "https://buf.build/docs/configuration/v2/buf-lock/#deps"},
		},
		{
			name: "dep_commit_key",
			line: 4, character: 4,
			expectedContains: []string{"commit", "commit hash", "https://buf.build/docs/configuration/v2/buf-lock/#deps"},
		},
		{
			name: "dep_digest_key",
			line: 5, character: 4,
			expectedContains: []string{"digest", "cryptographic digest", "https://buf.build/docs/configuration/v2/buf-lock/#deps"},
		},
		{
			// Comment line — not a YAML key.
			name: "comment_no_hover",
			line: 0, character: 0,
			expectNoHover: true,
		},
		{
			name: "off_file_no_hover",
			line: 999, character: 0,
			expectNoHover: true,
		},
	}

	absPath, err := filepath.Abs(fixture)
	require.NoError(t, err)

	clientJSONConn, bufLockURI, _ := setupLSPServerForBufYAML(t, absPath, nil, nil)
	ctx := t.Context()

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			var hover *protocol.Hover
			_, err := clientJSONConn.Call(ctx, protocol.MethodTextDocumentHover, &protocol.HoverParams{
				TextDocumentPositionParams: protocol.TextDocumentPositionParams{
					TextDocument: protocol.TextDocumentIdentifier{URI: bufLockURI},
					Position:     protocol.Position{Line: tc.line, Character: tc.character},
				},
			}, &hover)
			require.NoError(t, err)

			if tc.expectNoHover {
				assert.Nil(t, hover, "expected no hover at (%d, %d)", tc.line, tc.character)
				return
			}
			require.NotNil(t, hover, "expected hover at (%d, %d)", tc.line, tc.character)
			assert.Equal(t, protocol.Markdown, hover.Contents.Kind)
			for _, want := range tc.expectedContains {
				assert.Contains(t, hover.Contents.Value, want,
					"hover at (%d, %d) should contain %q", tc.line, tc.character, want)
			}
		})
	}
}
