// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package web_test

import (
	"bytes"
	"regexp"
	"runtime"
	"testing"

	"golang.org/x/net/html"
	"golang.org/x/tools/gopls/internal/protocol"
	"golang.org/x/tools/gopls/internal/settings"
	. "golang.org/x/tools/gopls/internal/test/integration"
	"golang.org/x/tools/internal/testenv"
)

// TestAssembly is a basic test of the web-based assembly listing.
func TestAssembly(t *testing.T) {
	testenv.NeedsGoCommand1Point(t, 22) // for up-to-date assembly listing

	const files = `
-- go.mod --
module example.com

-- a/a.go --
package a

func f(x int) int {
	println("hello")
	defer println("world")
	return x
}

func g() {
	println("goodbye")
}

var v = [...]int{
	f(123),
	f(456),
}

func init() {
	f(789)
}
`
	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("a/a.go")

		// Get the report and do some minimal checks for sensible results.
		//
		// Use only portable instructions below! Remember that
		// this is a test of plumbing, not compilation, so
		// it's better to skip the tests, rather than refine
		// them, on any architecture that gives us trouble
		// (e.g. uses JAL for CALL, or BL<cc> for RET).
		// We conservatively test only on the two most popular
		// architectures.
		{
			loc := env.RegexpSearch("a/a.go", "println")
			report := asmFor(t, env, loc)
			checkMatch(t, true, report, `TEXT.*example.com/a.f`)
			switch runtime.GOARCH {
			case "amd64", "arm64":
				checkMatch(t, true, report, `CALL	runtime.printlock`)
				checkMatch(t, true, report, `CALL	runtime.printstring`)
				checkMatch(t, true, report, `CALL	runtime.printunlock`)
				checkMatch(t, true, report, `CALL	example.com/a.f.deferwrap`)
				checkMatch(t, true, report, `RET`)
				checkMatch(t, true, report, `CALL	runtime.morestack_noctxt`)
			}

			// Nested functions are also shown.
			//
			// The condition here was relaxed to unblock go.dev/cl/639515.
			checkMatch(t, true, report, `example.com/a.f.deferwrap`)

			// But other functions are not.
			checkMatch(t, false, report, `TEXT.*example.com/a.g`)
		}

		// Check that code in a package-level var initializer is found too.
		{
			loc := env.RegexpSearch("a/a.go", `f\(123\)`)
			report := asmFor(t, env, loc)
			switch runtime.GOARCH {
			case "amd64", "arm64":
				checkMatch(t, true, report, `TEXT.*example.com/a.init`)
				checkMatch(t, true, report, `MOV.?	\$123`)
				checkMatch(t, true, report, `MOV.?	\$456`)
				checkMatch(t, true, report, `CALL	example.com/a.f`)
			}
		}

		// And code in a source-level init function.
		{
			loc := env.RegexpSearch("a/a.go", `f\(789\)`)
			report := asmFor(t, env, loc)
			switch runtime.GOARCH {
			case "amd64", "arm64":
				checkMatch(t, true, report, `TEXT.*example.com/a.init`)
				checkMatch(t, true, report, `MOV.?	\$789`)
				checkMatch(t, true, report, `CALL	example.com/a.f`)
			}
		}
	})
}

// TestTestAssembly exercises assembly listing of tests.
func TestTestAssembly(t *testing.T) {
	testenv.NeedsGoCommand1Point(t, 22) // for up-to-date assembly listing

	const files = `
-- go.mod --
module example.com

-- a/a_test.go --
package a

import "testing"

func Test1(*testing.T) { println(0) }

-- a/a_x_test.go --
package a_test

import "testing"

func Test2(*testing.T) { println(0) }
`
	Run(t, files, func(t *testing.T, env *Env) {
		for _, test := range []struct {
			filename, symbol string
		}{
			{"a/a_test.go", "example.com/a.Test1"},
			{"a/a_x_test.go", "example.com/a_test.Test2"},
		} {
			env.OpenFile(test.filename)
			loc := env.RegexpSearch(test.filename, `println`)
			report := asmFor(t, env, loc)
			checkMatch(t, true, report, `TEXT.*`+regexp.QuoteMeta(test.symbol))
			switch runtime.GOARCH {
			case "amd64", "arm64":
				checkMatch(t, true, report, `CALL	runtime.printint`)
			}
		}
	})
}

// TestAssemblyMisquote exercises that compiler output is properly quoted.
func TestAssemblyMisquote(t *testing.T) {
	const files = `
-- go.mod --
module example.com

-- a/a.go --
package a

func f() {
	// The compiler rejects this addition. The error it prints may include
	// the string literal but this should not cause the resulting HTML page
	// to contain a <u> element, which would result in misformatting.
	const _ = "</script><u>underline</u>" + 1
}
`
	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("a/a.go")
		loc := env.RegexpSearch("a/a.go", "const")
		report := asmFor(t, env, loc)
		doc, err := html.Parse(bytes.NewReader(report))
		if err != nil {
			t.Fatalf("html.Parse: %v", err)
		}
		for n := range doc.Descendants() {
			if n.Type == html.ElementNode && n.Data == "u" {
				t.Fatalf("unwanted <u> element in HTML document:\n%s", report)
			}
		}
	})
}

// asmFor returns the HTML document served by gopls for a "Browse assembly"
// command at the specified location in an open file.
func asmFor(t *testing.T, env *Env, loc protocol.Location) []byte {
	_, content := codeActionWebPage(t, env, settings.GoAssembly, loc)
	return content
}
