// Copyright 2024 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.

// For os.CopyFS
//go:build go1.23

package main

import (
	"bytes"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"testing"

	"golang.org/x/tools/internal/diffp"
	"golang.org/x/tools/internal/testenv"
	"golang.org/x/tools/txtar"
)

// This file contains a test that checks the output files existence
// and content when stringer has types from multiple different input
// files to choose from.
//
// Input is specified in a txtar string.

// Several tests expect the type Foo generated in some package.
func expectFooString(pkg string) []byte {
	return []byte(fmt.Sprintf(`
// Header comment ignored.

package %s

import "strconv"

func _() {
	// An "invalid array index" compiler error signifies that the constant values have changed.
	// Re-run the stringer command to generate them again.
	var x [1]struct{}
	_ = x[fooX-0]
	_ = x[fooY-1]
	_ = x[fooZ-2]
}

const _Foo_name = "fooXfooYfooZ"

var _Foo_index = [...]uint8{0, 4, 8, 12}

func (i Foo) String() string {
	if i < 0 || i >= Foo(len(_Foo_index)-1) {
		return "Foo(" + strconv.FormatInt(int64(i), 10) + ")"
	}
	return _Foo_name[_Foo_index[i]:_Foo_index[i+1]]
}`, pkg))
}

func TestMultifileStringer(t *testing.T) {
	testenv.NeedsTool(t, "go")
	stringer := stringerPath(t)

	tests := []struct {
		name        string
		args        []string
		archive     []byte
		expectFiles map[string][]byte
	}{
		{
			name: "package only",
			args: []string{"-type=Foo"},
			archive: []byte(`
-- go.mod --
module foo

-- main.go --
package main

type Foo int

const (
	fooX Foo = iota
	fooY
	fooZ
)`),
			expectFiles: map[string][]byte{
				"foo_string.go": expectFooString("main"),
			},
		},
		{
			name: "test package only",
			args: []string{"-type=Foo"},
			archive: []byte(`
-- go.mod --
module foo

-- main.go --
package main

func main() {}

-- main_test.go --
package main

type Foo int

const (
	fooX Foo = iota
	fooY
	fooZ
)`),
			expectFiles: map[string][]byte{
				"foo_string_test.go": expectFooString("main"),
			},
		},
		{
			name: "x_test package only",
			args: []string{"-type=Foo"},
			archive: []byte(`
-- go.mod --
module foo

-- main.go --
package main

func main() {}

-- main_test.go --
package main_test

type Foo int

const (
	fooX Foo = iota
	fooY
	fooZ
)`),
			expectFiles: map[string][]byte{
				"foo_string_test.go": expectFooString("main_test"),
			},
		},
		{
			// Re-declaring the type in a less prioritized package does not change our output.
			name: "package over test package",
			args: []string{"-type=Foo"},
			archive: []byte(`
-- go.mod --
module foo

-- main.go --
package main

type Foo int

const (
	fooX Foo = iota
	fooY
	fooZ
)

-- main_test.go --
package main

type Foo int

const (
	otherX Foo = iota
	otherY
	otherZ
)
`),
			expectFiles: map[string][]byte{
				"foo_string.go": expectFooString("main"),
			},
		},
		{
			// Re-declaring the type in a less prioritized package does not change our output.
			name: "package over x_test package",
			args: []string{"-type=Foo"},
			archive: []byte(`
-- go.mod --
module foo

-- main.go --
package main

type Foo int

const (
	fooX Foo = iota
	fooY
	fooZ
)

-- main_test.go --
package main_test

type Foo int

const (
	otherX Foo = iota
	otherY
	otherZ
)
`),
			expectFiles: map[string][]byte{
				"foo_string.go": expectFooString("main"),
			},
		},
		{
			// Re-declaring the type in a less prioritized package does not change our output.
			name: "test package over x_test package",
			args: []string{"-type=Foo"},
			archive: []byte(`
-- go.mod --
module foo

-- main.go --
package main

-- main_test.go --
package main

type Foo int

const (
	fooX Foo = iota
	fooY
	fooZ
)

-- main_pkg_test.go --
package main_test

type Foo int

const (
	otherX Foo = iota
	otherY
	otherZ
)`),
			expectFiles: map[string][]byte{
				"foo_string_test.go": expectFooString("main"),
			},
		},
		{
			name: "unique type in each package variant",
			args: []string{"-type=Foo,Bar,Baz"},
			archive: []byte(`
-- go.mod --
module foo

-- main.go --
package main

type Foo int

const fooX Foo = 1

-- main_test.go --
package main

type Bar int

const barX Bar = 1

-- main_pkg_test.go --
package main_test

type Baz int

const bazX Baz = 1
`),
			expectFiles: map[string][]byte{
				"foo_string.go": []byte(`
// Header comment ignored.

package main

import "strconv"

func _() {
	// An "invalid array index" compiler error signifies that the constant values have changed.
	// Re-run the stringer command to generate them again.
	var x [1]struct{}
	_ = x[fooX-1]
}

const _Foo_name = "fooX"

var _Foo_index = [...]uint8{0, 4}

func (i Foo) String() string {
	i -= 1
	if i < 0 || i >= Foo(len(_Foo_index)-1) {
		return "Foo(" + strconv.FormatInt(int64(i+1), 10) + ")"
	}
	return _Foo_name[_Foo_index[i]:_Foo_index[i+1]]
}`),

				"bar_string_test.go": []byte(`
// Header comment ignored.

package main

import "strconv"

func _() {
	// An "invalid array index" compiler error signifies that the constant values have changed.
	// Re-run the stringer command to generate them again.
	var x [1]struct{}
	_ = x[barX-1]
}

const _Bar_name = "barX"

var _Bar_index = [...]uint8{0, 4}

func (i Bar) String() string {
	i -= 1
	if i < 0 || i >= Bar(len(_Bar_index)-1) {
		return "Bar(" + strconv.FormatInt(int64(i+1), 10) + ")"
	}
	return _Bar_name[_Bar_index[i]:_Bar_index[i+1]]
}`),

				"baz_string_test.go": []byte(`
// Header comment ignored.

package main_test

import "strconv"

func _() {
	// An "invalid array index" compiler error signifies that the constant values have changed.
	// Re-run the stringer command to generate them again.
	var x [1]struct{}
	_ = x[bazX-1]
}

const _Baz_name = "bazX"

var _Baz_index = [...]uint8{0, 4}

func (i Baz) String() string {
	i -= 1
	if i < 0 || i >= Baz(len(_Baz_index)-1) {
		return "Baz(" + strconv.FormatInt(int64(i+1), 10) + ")"
	}
	return _Baz_name[_Baz_index[i]:_Baz_index[i+1]]
}`),
			},
		},

		{
			name: "package over test package with custom output",
			args: []string{"-type=Foo", "-output=custom_output.go"},
			archive: []byte(`
-- go.mod --
module foo

-- main.go --
package main

type Foo int

const (
	fooX Foo = iota
	fooY
	fooZ
)

-- main_test.go --
package main

type Foo int

const (
	otherX Foo = iota
	otherY
	otherZ
)`),
			expectFiles: map[string][]byte{
				"custom_output.go": expectFooString("main"),
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tmpDir := t.TempDir()

			arFS, err := txtar.FS(txtar.Parse(tt.archive))
			if err != nil {
				t.Fatalf("txtar.FS: %s", err)
			}
			err = os.CopyFS(tmpDir, arFS)
			if err != nil {
				t.Fatalf("copy fs: %s", err)
			}
			before := dirContent(t, tmpDir)

			// Must run stringer in the temp directory, see TestTags.
			args := append(tt.args, tmpDir)
			err = runInDir(t, tmpDir, stringer, args...)
			if err != nil {
				t.Fatalf("run stringer: %s", err)
			}

			// Check that all !path files have been created with the expected content.
			for f, want := range tt.expectFiles {
				got, err := os.ReadFile(filepath.Join(tmpDir, f))
				if errors.Is(err, os.ErrNotExist) {
					t.Errorf("expected file not written during test: %s", f)
					continue
				}
				if err != nil {
					t.Fatalf("read file %q: %s", f, err)
				}
				// Trim data for more robust comparison.
				got = trimHeader(bytes.TrimSpace(got))
				want = trimHeader(bytes.TrimSpace(want))
				if !bytes.Equal(want, got) {
					t.Errorf("file %s does not have the expected content:\n%s", f, diffp.Diff("want", want, "got", got))
				}
			}

			// Check that nothing else has been created.
			after := dirContent(t, tmpDir)
			for f := range after {
				if _, expected := tt.expectFiles[f]; !expected && !before[f] {
					t.Errorf("found %q in output directory, it is neither input or expected output", f)
				}
			}

		})
	}
}

func dirContent(t *testing.T, dir string) map[string]bool {
	entries, err := os.ReadDir(dir)
	if err != nil {
		t.Fatalf("read dir: %s", err)
	}

	out := map[string]bool{}
	for _, e := range entries {
		out[e.Name()] = true
	}
	return out
}

// trimHeader that stringer puts in file.
// It depends on location and interferes with comparing file content.
func trimHeader(s []byte) []byte {
	if !bytes.HasPrefix(s, []byte("//")) {
		return s
	}
	_, after, ok := bytes.Cut(s, []byte{'\n'})
	if ok {
		return after
	}
	return s
}
