package main

import (
	"bytes"
	"io"
	"os"
	"reflect"
	"regexp"
	"strings"
	"testing"

	"github.com/kisielk/errcheck/errcheck"
)

var dotStar = regexp.MustCompile(".*")

func TestMain(t *testing.T) {
	saveStderr := os.Stderr
	saveStdout := os.Stdout
	saveCwd, err := os.Getwd()
	if err != nil {
		t.Errorf("Cannot receive current directory: %v", err)
	}

	r, w, err := os.Pipe()
	if err != nil {
		t.Errorf("Cannot create pipe: %v", err)
	}

	os.Stderr = w
	os.Stdout = w

	bufChannel := make(chan string)

	go func() {
		buf := new(bytes.Buffer)
		_, err = io.Copy(buf, r)
		r.Close()
		if err != nil {
			t.Errorf("Cannot copy to buffer: %v", err)
		}

		bufChannel <- buf.String()
	}()

	exitCode := mainCmd([]string{"cmd name", "github.com/kisielk/errcheck/testdata"})

	w.Close()

	os.Stderr = saveStderr
	os.Stdout = saveStdout
	os.Chdir(saveCwd)

	out := <-bufChannel

	if exitCode != exitUncheckedError {
		t.Errorf("Exit code is %d, expected %d", exitCode, exitUncheckedError)
	}

	expectUnchecked := 29
	if got := strings.Count(out, "UNCHECKED"); got != expectUnchecked {
		t.Errorf("Got %d UNCHECKED errors, expected %d in:\n%s", got, expectUnchecked, out)
	}
}

type parseTestCase struct {
	args    []string
	paths   []string
	ignore  map[string]string // Exclusions.SymbolRegexpsByPackage
	pkgs    []string          // Exclusions.Packages
	tags    []string          // Tags
	blank   bool              // !BlankAssignments
	asserts bool              // !TypeAssertions
	error   int
}

func TestParseFlags(t *testing.T) {
	cases := []parseTestCase{
		parseTestCase{
			args:    []string{"errcheck"},
			paths:   []string{"."},
			ignore:  map[string]string{},
			pkgs:    []string{},
			tags:    []string{},
			blank:   false,
			asserts: false,
			error:   exitCodeOk,
		},
		parseTestCase{
			args:    []string{"errcheck", "-blank", "-asserts"},
			paths:   []string{"."},
			ignore:  map[string]string{},
			pkgs:    []string{},
			tags:    []string{},
			blank:   true,
			asserts: true,
			error:   exitCodeOk,
		},
		parseTestCase{
			args:    []string{"errcheck", "foo", "bar"},
			paths:   []string{"foo", "bar"},
			ignore:  map[string]string{},
			pkgs:    []string{},
			tags:    []string{},
			blank:   false,
			asserts: false,
			error:   exitCodeOk,
		},
		parseTestCase{
			args:    []string{"errcheck", "-ignore", "fmt:.*,encoding/binary:.*"},
			paths:   []string{"."},
			ignore:  map[string]string{"fmt": ".*", "encoding/binary": dotStar.String()},
			pkgs:    []string{},
			tags:    []string{},
			blank:   false,
			asserts: false,
			error:   exitCodeOk,
		},
		parseTestCase{
			args:    []string{"errcheck", "-ignore", "fmt:[FS]?[Pp]rint*"},
			paths:   []string{"."},
			ignore:  map[string]string{"fmt": "[FS]?[Pp]rint*"},
			pkgs:    []string{},
			tags:    []string{},
			blank:   false,
			asserts: false,
			error:   exitCodeOk,
		},
		parseTestCase{
			args:    []string{"errcheck", "-ignore", "[rR]ead|[wW]rite"},
			paths:   []string{"."},
			ignore:  map[string]string{"": "[rR]ead|[wW]rite"},
			pkgs:    []string{},
			tags:    []string{},
			blank:   false,
			asserts: false,
			error:   exitCodeOk,
		},
		parseTestCase{
			args:    []string{"errcheck", "-ignorepkg", "testing"},
			paths:   []string{"."},
			ignore:  map[string]string{},
			pkgs:    []string{"testing"},
			tags:    []string{},
			blank:   false,
			asserts: false,
			error:   exitCodeOk,
		},
		parseTestCase{
			args:    []string{"errcheck", "-ignorepkg", "testing,foo"},
			paths:   []string{"."},
			ignore:  map[string]string{},
			pkgs:    []string{"testing", "foo"},
			tags:    []string{},
			blank:   false,
			asserts: false,
			error:   exitCodeOk,
		},
		parseTestCase{
			args:    []string{"errcheck", "-tags", "foo"},
			paths:   []string{"."},
			ignore:  map[string]string{},
			pkgs:    []string{},
			tags:    []string{"foo"},
			blank:   false,
			asserts: false,
			error:   exitCodeOk,
		},
		parseTestCase{
			args:    []string{"errcheck", "-tags", "foo bar !baz"},
			paths:   []string{"."},
			ignore:  map[string]string{},
			pkgs:    []string{},
			tags:    []string{"foo", "bar", "!baz"},
			blank:   false,
			asserts: false,
			error:   exitCodeOk,
		},
		parseTestCase{
			args:    []string{"errcheck", "-tags", "foo,bar,!baz"},
			paths:   []string{"."},
			ignore:  map[string]string{},
			pkgs:    []string{},
			tags:    []string{"foo", "bar", "!baz"},
			blank:   false,
			asserts: false,
			error:   exitCodeOk,
		},
		parseTestCase{
			args:    []string{"errcheck", "-tags", "foo   bar   !baz"},
			paths:   []string{"."},
			ignore:  map[string]string{},
			pkgs:    []string{},
			tags:    []string{"foo", "bar", "!baz"},
			blank:   false,
			asserts: false,
			error:   exitCodeOk,
		},
	}

	slicesEqual := func(a, b []string) bool {
		if len(a) != len(b) {
			return false
		}
		for i := range a {
			if a[i] != b[i] {
				return false
			}
		}
		return true
	}

	ignoresEqual := func(a map[string]*regexp.Regexp, b map[string]string) bool {
		if (a == nil && b != nil) || (a != nil && b == nil) || (len(a) != len(b)) {
			return false
		}
		for k, v := range a {
			if v.String() != b[k] {
				return false
			}
		}
		return true
	}

	for _, c := range cases {
		var checker errcheck.Checker
		p, e := parseFlags(&checker, c.args)

		argsStr := strings.Join(c.args, " ")
		if !slicesEqual(p, c.paths) {
			t.Errorf("%q: path got %q want %q", argsStr, p, c.paths)
		}
		if ign := checker.Exclusions.SymbolRegexpsByPackage; !ignoresEqual(ign, c.ignore) {
			t.Errorf("%q: ignore got %q want %q", argsStr, ign, c.ignore)
		}
		if pkgs := checker.Exclusions.Packages; !slicesEqual(pkgs, c.pkgs) {
			t.Errorf("%q: packages got %v want %v", argsStr, pkgs, c.pkgs)
		}
		if tags := checker.Tags; !slicesEqual(tags, c.tags) {
			t.Errorf("%q: tags got %v want %v", argsStr, tags, c.tags)
		}
		if b := checker.Exclusions.BlankAssignments; b != !c.blank {
			t.Errorf("%q: BlankAssignments got %v want %v", argsStr, b, !c.blank)
		}
		if a := checker.Exclusions.TypeAssertions; a != !c.asserts {
			t.Errorf("%q: TypeAssertions got %v want %v", argsStr, a, !c.asserts)
		}
		if e != c.error {
			t.Errorf("%q: error got %q want %q", argsStr, e, c.error)
		}
	}
}

func TestReadExcludes(t *testing.T) {
	expectedExcludes := []string{
		"hello()",
		"world()",
	}
	t.Logf("expectedExcludes: %#v", expectedExcludes)
	excludes, err := readExcludes("testdata/excludes.txt")
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("excludes: %#v", excludes)
	if !reflect.DeepEqual(expectedExcludes, excludes) {
		t.Fatal("excludes did not match expectedExcludes")
	}
}

func TestReadEmptyExcludes(t *testing.T) {
	excludes, err := readExcludes("testdata/empty_excludes.txt")
	if err != nil {
		t.Fatal(err)
	}
	if len(excludes) != 0 {
		t.Fatalf("expected empty excludes, got %#v", excludes)
	}
}

func TestReadExcludesMissingFile(t *testing.T) {
	_, err := readExcludes("testdata/missing_file")
	if err == nil {
		t.Fatal("expected non-nil err, got nil")
	}
}
