// Copyright 2020 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 golang

import (
	"bytes"
	"go/ast"
	"go/parser"
	"go/token"
	"go/types"
	"testing"

	"golang.org/x/tools/go/ast/inspector"
)

func TestSearchForEnclosing(t *testing.T) {
	tests := []struct {
		desc string
		// For convenience, consider the first occurrence of the identifier "X" in
		// src.
		src string
		// By convention, "" means no type found.
		wantTypeName string
	}{
		{
			// TODO(rFindley): is this correct, or do we want to resolve I2 here?
			desc:         "embedded interface in interface",
			src:          `package a; var y = i1.X; type i1 interface {I2}; type I2 interface{X()}`,
			wantTypeName: "",
		},
		{
			desc:         "embedded interface in struct",
			src:          `package a; var y = t.X; type t struct {I}; type I interface{X()}`,
			wantTypeName: "I",
		},
		{
			desc:         "double embedding",
			src:          `package a; var y = t1.X; type t1 struct {t2}; type t2 struct {I}; type I interface{X()}`,
			wantTypeName: "I",
		},
	}

	for _, test := range tests {
		t.Run(test.desc, func(t *testing.T) {
			fset := token.NewFileSet()
			file, err := parser.ParseFile(fset, "a.go", test.src, parser.AllErrors|parser.SkipObjectResolution)
			if err != nil {
				t.Fatal(err)
			}
			var (
				files  = []*ast.File{file}
				column = 1 + bytes.IndexRune([]byte(test.src), 'X')
				pos    = posAt(1, column, fset, "a.go")
			)
			cur, ok := inspector.New(files).Root().FindByPos(pos, pos)
			if !ok {
				t.Fatalf("no ident found at (1, %d)", column)
			}
			info := newInfo()
			if _, err = (*types.Config)(nil).Check("p", fset, files, info); err != nil {
				t.Fatal(err)
			}
			obj := searchForEnclosing(info, cur)
			if obj == nil {
				if test.wantTypeName != "" {
					t.Errorf("searchForEnclosing(...) = <nil>, want %q", test.wantTypeName)
				}
				return
			}
			if got := obj.Name(); got != test.wantTypeName {
				t.Errorf("searchForEnclosing(...) = %q, want %q", got, test.wantTypeName)
			}
		})
	}
}

// posAt returns the token.Pos corresponding to the 1-based (line, column)
// coordinates in the file fname of fset.
func posAt(line, column int, fset *token.FileSet, fname string) token.Pos {
	var tok *token.File
	fset.Iterate(func(tf *token.File) bool {
		if tf.Name() == fname {
			tok = tf
			return false
		}
		return true
	})
	if tok == nil {
		return token.NoPos
	}
	start := tok.LineStart(line)
	return start + token.Pos(column-1)
}

// newInfo returns a types.Info with all maps populated.
func newInfo() *types.Info {
	info := &types.Info{
		Types:        make(map[ast.Expr]types.TypeAndValue),
		Defs:         make(map[*ast.Ident]types.Object),
		Uses:         make(map[*ast.Ident]types.Object),
		Implicits:    make(map[ast.Node]types.Object),
		Selections:   make(map[*ast.SelectorExpr]*types.Selection),
		Scopes:       make(map[ast.Node]*types.Scope),
		FileVersions: make(map[*ast.File]string),
	}
	return info
}
