// 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 main

import (
	"bytes"
	"fmt"
	"go/parser"
	"go/token"
	"html/template"
	"strings"
	"testing"
)

func TestImports(t *testing.T) {
	t.Run("importName", func(t *testing.T) {
		cases := []struct {
			src   string
			ident string
		}{
			{`"syscall"`, "syscall"},
			{`. "foobar"`, "."},
			{`"go/ast"`, "ast"},
			{`moo "go/format"`, "moo"},
			{`. "go/token"`, "."},
			{`"golang.org/x/sys/unix"`, "unix"},
			{`nix "golang.org/x/sys/unix"`, "nix"},
			{`_ "golang.org/x/sys/unix"`, "_"},
		}

		for _, c := range cases {
			pkgSrc := fmt.Sprintf("package main\nimport %s", c.src)

			f, err := parser.ParseFile(token.NewFileSet(), "", pkgSrc, parser.ImportsOnly)
			if err != nil {
				t.Error(err)
				continue
			}
			if len(f.Imports) != 1 {
				t.Errorf("Got %d imports, expected 1", len(f.Imports))
				continue
			}

			got, err := importName(f.Imports[0])
			if err != nil {
				t.Fatal(err)
			}
			if got != c.ident {
				t.Errorf("Got %q, expected %q", got, c.ident)
			}
		}
	})

	t.Run("filterImports", func(t *testing.T) {
		cases := []struct{ before, after string }{
			{`package test

			import (
				"foo"
				"bar"
			)`,
				"package test\n"},
			{`package test

			import (
				"foo"
				"bar"
			)

			func useFoo() { foo.Usage() }`,
				`package test

import (
	"foo"
)

func useFoo() { foo.Usage() }
`},
		}
		for _, c := range cases {
			got, err := filterImports([]byte(c.before))
			if err != nil {
				t.Error(err)
			}

			if string(got) != c.after {
				t.Errorf("Got:\n%s\nExpected:\n%s\n", got, c.after)
			}
		}
	})
}

func TestMerge(t *testing.T) {
	// Input architecture files
	inTmpl := template.Must(template.New("input").Parse(`
// Package comments

// build directives for arch{{.}}

//go:build goos && arch{{.}}

package main

/*
#include <stdint.h>
#include <stddef.h>
int utimes(uintptr_t, uintptr_t);
int utimensat(int, uintptr_t, uintptr_t, int);
*/
import "C"

// The imports
import (
	"commonDep"
	"uniqueDep{{.}}"
)

// Vars
var (
	commonVar = commonDep.Use("common")

	uniqueVar{{.}} = "unique{{.}}"
)

// Common free standing comment

// Common comment
const COMMON_INDEPENDENT = 1234
const UNIQUE_INDEPENDENT_{{.}} = "UNIQUE_INDEPENDENT_{{.}}"

// Group comment
const (
	COMMON_GROUP = "COMMON_GROUP"
	UNIQUE_GROUP_{{.}} = "UNIQUE_GROUP_{{.}}"
)

// Group2 comment
const (
	UNIQUE_GROUP21_{{.}} = "UNIQUE_GROUP21_{{.}}"
	UNIQUE_GROUP22_{{.}} = "UNIQUE_GROUP22_{{.}}"
)

// Group3 comment
const (
	sub1Common1 = 11
	sub1Unique2{{.}} = 12
	sub1Common3_LONG = 13

	sub2Unique1{{.}} = 21
	sub2Common2 = 22
	sub2Common3 = 23
	sub2Unique4{{.}} = 24
)

type commonInt int

type uniqueInt{{.}} int

func commonF() string {
	return commonDep.Use("common")
	}

func uniqueF() string {
	C.utimes(0, 0)
	return uniqueDep{{.}}.Use("{{.}}")
	}

// Group4 comment
const (
	sub3Common1 = 31
	sub3Unique2{{.}} = 32
	sub3Unique3{{.}} = 33
	sub3Common4 = 34

	sub4Common1, sub4Unique2{{.}} = 41, 42
	sub4Unique3{{.}}, sub4Common4 = 43, 44
)
`))

	// Filtered architecture files
	outTmpl := template.Must(template.New("output").Parse(`// Package comments

// build directives for arch{{.}}

//go:build goos && arch{{.}}

package main

/*
#include <stdint.h>
#include <stddef.h>
int utimes(uintptr_t, uintptr_t);
int utimensat(int, uintptr_t, uintptr_t, int);
*/
import "C"

// The imports
import (
	"commonDep"
	"uniqueDep{{.}}"
)

// Vars
var (
	commonVar = commonDep.Use("common")

	uniqueVar{{.}} = "unique{{.}}"
)

const UNIQUE_INDEPENDENT_{{.}} = "UNIQUE_INDEPENDENT_{{.}}"

// Group comment
const (
	UNIQUE_GROUP_{{.}} = "UNIQUE_GROUP_{{.}}"
)

// Group2 comment
const (
	UNIQUE_GROUP21_{{.}} = "UNIQUE_GROUP21_{{.}}"
	UNIQUE_GROUP22_{{.}} = "UNIQUE_GROUP22_{{.}}"
)

// Group3 comment
const (
	sub1Unique2{{.}} = 12

	sub2Unique1{{.}} = 21
	sub2Unique4{{.}} = 24
)

type uniqueInt{{.}} int

func uniqueF() string {
	C.utimes(0, 0)
	return uniqueDep{{.}}.Use("{{.}}")
}

// Group4 comment
const (
	sub3Unique2{{.}} = 32
	sub3Unique3{{.}} = 33

	sub4Common1, sub4Unique2{{.}} = 41, 42
	sub4Unique3{{.}}, sub4Common4 = 43, 44
)
`))

	const mergedFile = `// Package comments

package main

// The imports
import (
	"commonDep"
)

// Common free standing comment

// Common comment
const COMMON_INDEPENDENT = 1234

// Group comment
const (
	COMMON_GROUP = "COMMON_GROUP"
)

// Group3 comment
const (
	sub1Common1      = 11
	sub1Common3_LONG = 13

	sub2Common2 = 22
	sub2Common3 = 23
)

type commonInt int

func commonF() string {
	return commonDep.Use("common")
}

// Group4 comment
const (
	sub3Common1 = 31
	sub3Common4 = 34
)
`

	// Generate source code for different "architectures"
	var inFiles, outFiles []srcFile
	for _, arch := range strings.Fields("A B C D") {
		buf := new(bytes.Buffer)
		err := inTmpl.Execute(buf, arch)
		if err != nil {
			t.Fatal(err)
		}
		inFiles = append(inFiles, srcFile{"file" + arch, buf.Bytes()})

		buf = new(bytes.Buffer)
		err = outTmpl.Execute(buf, arch)
		if err != nil {
			t.Fatal(err)
		}
		outFiles = append(outFiles, srcFile{"file" + arch, buf.Bytes()})
	}

	t.Run("getCodeSet", func(t *testing.T) {
		got, err := getCodeSet(inFiles[0].src)
		if err != nil {
			t.Fatal(err)
		}

		expectedElems := []codeElem{
			{token.COMMENT, "Package comments\n"},
			{token.COMMENT, "build directives for archA\n"},
			{token.COMMENT, "go:build goos && archA\n"},
			{token.CONST, `COMMON_INDEPENDENT = 1234`},
			{token.CONST, `UNIQUE_INDEPENDENT_A = "UNIQUE_INDEPENDENT_A"`},
			{token.CONST, `COMMON_GROUP = "COMMON_GROUP"`},
			{token.CONST, `UNIQUE_GROUP_A = "UNIQUE_GROUP_A"`},
			{token.CONST, `UNIQUE_GROUP21_A = "UNIQUE_GROUP21_A"`},
			{token.CONST, `UNIQUE_GROUP22_A = "UNIQUE_GROUP22_A"`},
			{token.CONST, `sub1Common1 = 11`},
			{token.CONST, `sub1Unique2A = 12`},
			{token.CONST, `sub1Common3_LONG = 13`},
			{token.CONST, `sub2Unique1A = 21`},
			{token.CONST, `sub2Common2 = 22`},
			{token.CONST, `sub2Common3 = 23`},
			{token.CONST, `sub2Unique4A = 24`},
			{token.CONST, `sub3Common1 = 31`},
			{token.CONST, `sub3Unique2A = 32`},
			{token.CONST, `sub3Unique3A = 33`},
			{token.CONST, `sub3Common4 = 34`},
			{token.CONST, `sub4Common1, sub4Unique2A = 41, 42`},
			{token.CONST, `sub4Unique3A, sub4Common4 = 43, 44`},
			{token.TYPE, `commonInt int`},
			{token.TYPE, `uniqueIntA int`},
			{token.FUNC, `func commonF() string {
	return commonDep.Use("common")
}`},
			{token.FUNC, `func uniqueF() string {
	C.utimes(0, 0)
	return uniqueDepA.Use("A")
}`},
		}
		expected := newCodeSet()
		for _, d := range expectedElems {
			expected.add(d)
		}

		if len(got.set) != len(expected.set) {
			t.Errorf("Got %d codeElems, expected %d", len(got.set), len(expected.set))
		}
		for expElem := range expected.set {
			if !got.has(expElem) {
				t.Errorf("Didn't get expected codeElem %#v", expElem)
			}
		}
		for gotElem := range got.set {
			if !expected.has(gotElem) {
				t.Errorf("Got unexpected codeElem %#v", gotElem)
			}
		}
	})

	t.Run("getCommonSet", func(t *testing.T) {
		got, err := getCommonSet(inFiles)
		if err != nil {
			t.Fatal(err)
		}

		expected := newCodeSet()
		expected.add(codeElem{token.COMMENT, "Package comments\n"})
		expected.add(codeElem{token.CONST, `COMMON_INDEPENDENT = 1234`})
		expected.add(codeElem{token.CONST, `COMMON_GROUP = "COMMON_GROUP"`})
		expected.add(codeElem{token.CONST, `sub1Common1 = 11`})
		expected.add(codeElem{token.CONST, `sub1Common3_LONG = 13`})
		expected.add(codeElem{token.CONST, `sub2Common2 = 22`})
		expected.add(codeElem{token.CONST, `sub2Common3 = 23`})
		expected.add(codeElem{token.CONST, `sub3Common1 = 31`})
		expected.add(codeElem{token.CONST, `sub3Common4 = 34`})
		expected.add(codeElem{token.TYPE, `commonInt int`})
		expected.add(codeElem{token.FUNC, `func commonF() string {
	return commonDep.Use("common")
}`})

		if len(got.set) != len(expected.set) {
			t.Errorf("Got %d codeElems, expected %d", len(got.set), len(expected.set))
		}
		for expElem := range expected.set {
			if !got.has(expElem) {
				t.Errorf("Didn't get expected codeElem %#v", expElem)
			}
		}
		for gotElem := range got.set {
			if !expected.has(gotElem) {
				t.Errorf("Got unexpected codeElem %#v", gotElem)
			}
		}
	})

	t.Run("filter(keepCommon)", func(t *testing.T) {
		commonSet, err := getCommonSet(inFiles)
		if err != nil {
			t.Fatal(err)
		}

		got, err := filter(inFiles[0].src, commonSet.keepCommon)
		if err != nil {
			t.Fatal(err)
		}

		expected := []byte(mergedFile)

		if !bytes.Equal(got, expected) {
			t.Errorf("Got:\n%s\nExpected:\n%s", addLineNr(got), addLineNr(expected))
			diffLines(t, got, expected)
		}
	})

	t.Run("filter(keepArchSpecific)", func(t *testing.T) {
		commonSet, err := getCommonSet(inFiles)
		if err != nil {
			t.Fatal(err)
		}

		for i := range inFiles {
			got, err := filter(inFiles[i].src, commonSet.keepArchSpecific)
			if err != nil {
				t.Fatal(err)
			}

			expected := outFiles[i].src

			if !bytes.Equal(got, expected) {
				t.Errorf("Got:\n%s\nExpected:\n%s", addLineNr(got), addLineNr(expected))
				diffLines(t, got, expected)
			}
		}
	})
}

func TestMergedName(t *testing.T) {
	t.Run("getValidGOOS", func(t *testing.T) {
		testcases := []struct {
			filename, goos string
			ok             bool
		}{
			{"zerrors_aix.go", "aix", true},
			{"zerrors_darwin.go", "darwin", true},
			{"zerrors_dragonfly.go", "dragonfly", true},
			{"zerrors_freebsd.go", "freebsd", true},
			{"zerrors_linux.go", "linux", true},
			{"zerrors_netbsd.go", "netbsd", true},
			{"zerrors_openbsd.go", "openbsd", true},
			{"zerrors_solaris.go", "solaris", true},
			{"zerrors_multics.go", "", false},
		}
		for _, tc := range testcases {
			goos, ok := getValidGOOS(tc.filename)
			if goos != tc.goos {
				t.Errorf("got GOOS %q, expected %q", goos, tc.goos)
			}
			if ok != tc.ok {
				t.Errorf("got ok %v, expected %v", ok, tc.ok)
			}
		}
	})
}

// Helper functions to diff test sources

func diffLines(t *testing.T, got, expected []byte) {
	t.Helper()

	gotLines := bytes.Split(got, []byte{'\n'})
	expLines := bytes.Split(expected, []byte{'\n'})

	i := 0
	for i < len(gotLines) && i < len(expLines) {
		if !bytes.Equal(gotLines[i], expLines[i]) {
			t.Errorf("Line %d: Got:\n%q\nExpected:\n%q", i+1, gotLines[i], expLines[i])
			return
		}
		i++
	}

	if i < len(gotLines) && i >= len(expLines) {
		t.Errorf("Line %d: got %q, expected EOF", i+1, gotLines[i])
	}
	if i >= len(gotLines) && i < len(expLines) {
		t.Errorf("Line %d: got EOF, expected %q", i+1, gotLines[i])
	}
}

func addLineNr(src []byte) []byte {
	lines := bytes.Split(src, []byte("\n"))
	for i, line := range lines {
		lines[i] = []byte(fmt.Sprintf("%d: %s", i+1, line))
	}
	return bytes.Join(lines, []byte("\n"))
}
