// Copyright 2018 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 expect_test

import (
	"bytes"
	"go/token"
	"os"
	"reflect"
	"slices"
	"testing"

	"golang.org/x/tools/internal/expect"
)

func TestMarker(t *testing.T) {
	for _, tt := range []struct {
		filename      string
		expectNotes   int
		expectMarkers map[string]string
		expectChecks  map[string][]any
		// expectChecks holds {"id": values} for each call check(id, values...).
		// Any named k=v arguments become a final map[string]any argument.
	}{
		{
			filename:    "testdata/test.go",
			expectNotes: 14,
			expectMarkers: map[string]string{
				"αSimpleMarker": "α",
				"OffsetMarker":  "β",
				"RegexMarker":   "γ",
				"εMultiple":     "ε",
				"ζMarkers":      "ζ",
				"ηBlockMarker":  "η",
				"Declared":      "η",
				"Comment":       "ι",
				"LineComment":   "someFunc",
				"NonIdentifier": "+",
				"StringMarker":  "\"hello\"",
			},
			expectChecks: map[string][]any{
				"αSimpleMarker": nil,
				"StringAndInt":  {"Number %d", int64(12)},
				"Bool":          {true},
				"NamedArgs": {int64(1), true, expect.Identifier("a"), map[string]any{
					"b": int64(1),
					"c": "3",
					"d": true,
				}},
			},
		},
		{
			filename:    "testdata/go.fake.mod",
			expectNotes: 2,
			expectMarkers: map[string]string{
				"αMarker": "αfake1α",
				"βMarker": "require golang.org/modfile v0.0.0",
			},
		},
		{
			filename:    "testdata/go.fake.work",
			expectNotes: 2,
			expectMarkers: map[string]string{
				"αMarker": "1.23.0",
				"βMarker": "αβ",
			},
		},
	} {
		t.Run(tt.filename, func(t *testing.T) {
			content, err := os.ReadFile(tt.filename)
			if err != nil {
				t.Fatal(err)
			}
			readFile := func(string) ([]byte, error) { return content, nil }

			markers := make(map[string]token.Pos)
			for name, tok := range tt.expectMarkers {
				offset := bytes.Index(content, []byte(tok))
				markers[name] = token.Pos(offset + 1)
				end := bytes.Index(content[offset:], []byte(tok))
				if end > 0 {
					markers[name+"@"] = token.Pos(offset + end + 2)
				}
			}

			fset := token.NewFileSet()
			notes, err := expect.Parse(fset, tt.filename, content)
			if err != nil {
				t.Fatalf("Failed to extract notes:\n%v", err)
			}
			if len(notes) != tt.expectNotes {
				t.Errorf("Expected %v notes, got %v", tt.expectNotes, len(notes))
			}
			for _, n := range notes {
				switch {
				case n.Args == nil:
					// A //@foo note associates the name foo with the position of the
					// first match of "foo" on the current line.
					checkMarker(t, fset, readFile, markers, n.Pos, n.Name, n.Name)
				case n.Name == "mark":
					// A //@mark(name, "pattern") note associates the specified name
					// with the position on the first match of pattern on the current line.
					if len(n.Args) != 2 {
						t.Errorf("%v: expected 2 args to mark, got %v", fset.Position(n.Pos), len(n.Args))
						continue
					}
					ident, ok := n.Args[0].(expect.Identifier)
					if !ok {
						t.Errorf("%v: got %v (%T), want identifier", fset.Position(n.Pos), n.Args[0], n.Args[0])
						continue
					}
					checkMarker(t, fset, readFile, markers, n.Pos, string(ident), n.Args[1])

				case n.Name == "check":
					// A //@check(args, ...) note specifies some hypothetical action to
					// be taken by the test driver and its expected outcome.
					// In this test, the action is to compare the arguments
					// against expectChecks.
					if len(n.Args) < 1 {
						t.Errorf("%v: expected 1 args to check, got %v", fset.Position(n.Pos), len(n.Args))
						continue
					}
					ident, ok := n.Args[0].(expect.Identifier)
					if !ok {
						t.Errorf("%v: got %v (%T), want identifier", fset.Position(n.Pos), n.Args[0], n.Args[0])
						continue
					}
					wantArgs, ok := tt.expectChecks[string(ident)]
					if !ok {
						t.Errorf("%v: unexpected check %v", fset.Position(n.Pos), ident)
						continue
					}
					gotArgs := n.Args[1:]
					if n.NamedArgs != nil {
						// Clip to avoid mutating Args' array.
						gotArgs = append(slices.Clip(gotArgs), n.NamedArgs)
					}

					if len(gotArgs) != len(wantArgs) {
						t.Errorf("%v: expected %v args to check, got %v", fset.Position(n.Pos), len(wantArgs), len(gotArgs))
						continue
					}
					for i := range gotArgs {
						if !reflect.DeepEqual(wantArgs[i], gotArgs[i]) {
							t.Errorf("%v: arg %d: expected %#v, got %#v", fset.Position(n.Pos), i+1, wantArgs[i], gotArgs[i])
						}
					}
				default:
					t.Errorf("Unexpected note %v at %v", n.Name, fset.Position(n.Pos))
				}
			}
		})
	}
}

func checkMarker(t *testing.T, fset *token.FileSet, readFile expect.ReadFile, markers map[string]token.Pos, pos token.Pos, name string, pattern any) {
	start, end, err := expect.MatchBefore(fset.File(pos), readFile, pos, pattern)
	if err != nil {
		t.Errorf("%v: MatchBefore failed: %v", fset.Position(pos), err)
		return
	}
	if start == token.NoPos {
		t.Errorf("%v: Pattern %v did not match", fset.Position(pos), pattern)
		return
	}
	expectStart, ok := markers[name]
	if !ok {
		t.Errorf("%v: unexpected marker %v", fset.Position(pos), name)
		return
	}
	if start != expectStart {
		t.Errorf("%v: Expected %v got %v", fset.Position(pos), fset.Position(expectStart), fset.Position(start))
	}
	if expectEnd, ok := markers[name+"@"]; ok && end != expectEnd {
		t.Errorf("%v: Expected end %v got %v", fset.Position(pos), fset.Position(expectEnd), fset.Position(end))
	}
}
