// Copyright 2017 Google Inc.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package getopt

import (
	"bytes"
	"fmt"
	"os"
	"reflect"
	"runtime"
	"strings"
	"testing"
	"time"
)

func TestGeneric(t *testing.T) {
	const (
		shortTest = iota
		longTest
		bothTest
	)
	for _, tt := range []struct {
		where string
		kind  int
		val   interface{}
		str   string
		def   interface{}
		in    []string
		err   string
	}{
		// Do all four tests for string, the rest can mostly just use
		// shortTest (the 0 value).
		{
			where: loc(),
			kind:  shortTest,
			val:   "42",
			str:   "42",
			in:    []string{"test", "-s", "42"},
		},
		{
			where: loc(),
			kind:  longTest,
			val:   "42",
			str:   "42",
			in:    []string{"test", "--long", "42"},
		},
		{
			where: loc(),
			kind:  bothTest,
			val:   "42",
			str:   "42",
			in:    []string{"test", "--both", "42"},
		},
		{
			where: loc(),
			kind:  bothTest,
			val:   "42",
			str:   "42",
			in:    []string{"test", "-b", "42"},
		},
		{
			where: loc(),
			val:   "42",
			def:   "42",
			str:   "42",
			in:    []string{"test"},
		},
		{
			where: loc(),
			val:   "42",
			def:   "43",
			str:   "42",
			in:    []string{"test", "-s", "42"},
		},

		{
			where: loc(),
			val:   true,
			str:   "true",
			in:    []string{"test", "-s"},
		},
		{
			where: loc(),
			val:   true,
			def:   true,
			str:   "true",
			in:    []string{"test"},
		},
		{
			where: loc(),
			kind:  longTest,
			val:   false,
			str:   "false",
			in:    []string{"test", "--long=false"},
		},
		{
			where: loc(),
			kind:  longTest,
			val:   false,
			def:   true,
			str:   "false",
			in:    []string{"test", "--long=false"},
		},

		{
			where: loc(),
			val:   int(42),
			str:   "42",
			in:    []string{"test", "-s", "42"},
		},
		{
			where: loc(),
			val:   int8(42),
			str:   "42",
			in:    []string{"test", "-s", "42"},
		},
		{
			where: loc(),
			val:   int16(42),
			str:   "42",
			in:    []string{"test", "-s", "42"},
		},
		{
			where: loc(),
			val:   int32(42),
			str:   "42",
			in:    []string{"test", "-s", "42"},
		},
		{
			where: loc(),
			val:   int64(42),
			str:   "42",
			in:    []string{"test", "-s", "42"},
		},

		{
			where: loc(),
			val:   uint(42),
			str:   "42",
			in:    []string{"test", "-s", "42"},
		},
		{
			where: loc(),
			val:   uint8(42),
			str:   "42",
			in:    []string{"test", "-s", "42"},
		},
		{
			where: loc(),
			val:   uint16(42),
			str:   "42",
			in:    []string{"test", "-s", "42"},
		},
		{
			where: loc(),
			val:   uint32(42),
			str:   "42",
			in:    []string{"test", "-s", "42"},
		},
		{
			where: loc(),
			val:   uint64(42),
			str:   "42",
			in:    []string{"test", "-s", "42"},
		},

		{
			where: loc(),
			val:   float32(4.2),
			str:   "4.2",
			in:    []string{"test", "-s", "4.2"},
		},
		{
			where: loc(),
			val:   float64(4.2),
			str:   "4.2",
			in:    []string{"test", "-s", "4.2"},
		},

		{
			where: loc(),
			val:   time.Duration(time.Second * 42),
			def:   time.Second * 2,
			str:   "42s",
			in:    []string{"test", "-s", "42s"},
		},
		{
			where: loc(),
			val:   time.Duration(time.Second * 42),
			def:   time.Second * 2,
			str:   "42s",
			in:    []string{"test", "-s42s"},
		},
		{
			where: loc(),
			val:   time.Duration(time.Second * 2),
			def:   time.Second * 2,
			in:    []string{"test", "-s42"},
			str:   "2s",
			err:   "test: time: missing unit in duration",
		},

		{
			where: loc(),
			val:   []string{"42", "."},
			str:   "42,.",
			def:   []string{"one", "two", "three"},
			in:    []string{"test", "-s42", "-s."},
		},
		{
			where: loc(),
			val:   []string{"42", "."},
			str:   "42,.",
			def:   []string{"one", "two", "three"},
			in:    []string{"test", "-s42,."},
		},
		{
			where: loc(),
			val:   []string{"one", "two", "three"},
			def:   []string{"one", "two", "three"},
			str:   "one,two,three",
			in:    []string{"test"},
		},
	} {
		reset()
		var opt Option
		val := reflect.New(reflect.TypeOf(tt.val)).Interface()
		if tt.def != nil {
			reflect.ValueOf(val).Elem().Set(reflect.ValueOf(tt.def))
		}
		switch tt.kind {
		case shortTest:
			opt = Flag(val, 's')
		case longTest:
			opt = FlagLong(val, "long", 0)
		case bothTest:
			opt = FlagLong(val, "both", 'b')
		}
		_ = opt
		parse(tt.in)
		if s := checkError(tt.err); s != "" {
			t.Errorf("%s: %s", tt.where, s)
			continue
		}
		got := reflect.ValueOf(val).Elem().Interface()
		want := reflect.ValueOf(tt.val).Interface()
		if !reflect.DeepEqual(got, want) {
			t.Errorf("%s: got %v, want %v", tt.where, got, want)
		}
		if str := opt.String(); str != tt.str {
			t.Errorf("%s: got string %q, want %q", tt.where, str, tt.str)
		}
	}
}

func TestGenericDup(t *testing.T) {
	defer func() {
		stderr = os.Stderr
		exit = os.Exit
	}()

	reset()
	var v1, v2 string
	type myPanic struct{}
	var errbuf bytes.Buffer
	stderr = &errbuf
	_, file, line, _ := runtime.Caller(0)
	Flag(&v1, 's')
	line++ // line is now the line number of the first call to Flag.

	exit = func(i int) { panic(myPanic{}) }
	defer func() {
		p := recover()
		if _, ok := p.(myPanic); ok {
			err := errbuf.String()
			if !strings.Contains(err, "-s already declared") || !strings.Contains(err, fmt.Sprintf("%s:%d", file, line)) {
				t.Errorf("unexpected error: %q\nshould contain \"-s already declared\" and \"%s:%d\"", err, file, line)
			}
		} else if p == nil {
			t.Errorf("Second call to Flag did not fail")
		} else {
			t.Errorf("panic %v", p)
		}
	}()
	Flag(&v2, 's')
}

func TestGenericDupNested(t *testing.T) {
	defer func() {
		stderr = os.Stderr
		exit = os.Exit
	}()

	reset()
	type myPanic struct{}
	var errbuf bytes.Buffer
	stderr = &errbuf
	_, file, line, _ := runtime.Caller(0)
	String('s', "default")
	line++ // line is now the line number of the first call to Flag.

	exit = func(i int) { panic(myPanic{}) }
	defer func() {
		p := recover()
		if _, ok := p.(myPanic); ok {
			err := errbuf.String()
			if !strings.Contains(err, "-s already declared") || !strings.Contains(err, fmt.Sprintf("%s:%d", file, line)) {
				t.Errorf("unexpected error: %q\nshould contain \"-s already declared\" and \"%s:%d\"", err, file, line)
			}
		} else if p == nil {
			t.Errorf("Second call to Flag did not fail")
		} else {
			t.Errorf("panic %v", p)
		}
	}()
	String('s', "default")
}
