package rags

import (
	"bytes"
	"flag"
	"fmt"
	"strconv"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

type tType struct {
	person string
	age    int
}

func (t *tType) Set(s string) error {
	v := strings.Split(s, ":")
	age, err := strconv.Atoi(v[1])
	if err != nil {
		return err
	}
	*t = tType{
		person: v[0],
		age:    age,
	}
	return nil
}

func (t *tType) String() string {
	if t.person == "" || t.age == 0 {
		return ""
	}
	return fmt.Sprintf("%s is %d years old", t.person, t.age)
}
func (t *tType) Get() any { return *t }

// tTypedValue is a test implementation of the TypedValue interface for the test type tType
type tTypedValue struct{ *tType }

func (t *tTypedValue) Get() any     { return *t.tType }
func (t *tTypedValue) Type() string { return "tvalue" }

// Verify that Rags get applied to underlying FlagSet correctly
func TestRagSet_FlagSet_Binding(t *testing.T) {
	t.Parallel()

	flags := []*Rag{
		{
			Name:  "help",
			Short: "h",
			Usage: "display help information for a command",
			Value: &Bool{},
		},
		{
			Name:  "log-level",
			Short: "v",
			Usage: "control verbosity. a higher number means chattier logs",
			Value: &Bool{},
		},
		{
			Name:  "log-json",
			Usage: "emit json logs",
			Value: &Bool{},
		},
		{
			Name:  "foo",
			Usage: "i foo the bar. see --bar",
			Value: &String{},
		},
		{
			Name:  "bar",
			Usage: "dont see me",
			Value: &Int{},
		},
		{
			Name:  "def-value",
			Short: "d",
			Value: NewValueDefault(new(string), "foo"),
		},
		{
			Name:  "def-value-ptr",
			Value: NewValueDefault(new(int), 100),
		},
		{
			Name:  "custom-value",
			Value: &tType{},
		},
	}

	testFlagSetBinding := func(t *testing.T, rs *RagSet, flags ...*Rag) {
		t.Helper()

		fs := rs.FlagSet()

		var expGoFlags = []string{}

		for _, f := range flags {
			for _, n := range f.Names() {
				expGoFlags = append(expGoFlags, n)

				gof := fs.Lookup(n)
				assert.NotNil(t, gof)

				fval := f.Value
				assert.Same(t, fval, gof.Value)
				assert.Implements(t, (*flag.Getter)(nil), gof.Value)
				getter, _ := gof.Value.(flag.Getter)
				assert.Equal(t, fval.Get(), getter.Get())
			}
		}

		actualCount := 0
		fs.VisitAll(func(f *flag.Flag) {
			actualCount++
			found := false
			for _, name := range expGoFlags {
				if name == f.Name {
					found = true
					break
				}
			}
			assert.True(t, found, "%s wasnt in expected flags %s", f.Name, expGoFlags)
		})
		assert.Equal(t, len(expGoFlags), actualCount)
	}

	t.Run("New_Add_Flags", func(t *testing.T) {
		rs := New("test-add-on-create", flag.ContinueOnError, flags...)
		assert.Len(t, rs.rags, 8)
		testFlagSetBinding(t, rs, flags...)
	})

	t.Run("Add", func(t *testing.T) {
		rs := New("test-add", flag.ContinueOnError)
		rs.Add(flags...)
		assert.Len(t, rs.rags, 8)
		testFlagSetBinding(t, rs, flags...)
	})
}

func TestRagUsageLine(t *testing.T) {
	var (
		valDst      = &tType{person: "tom", age: 100}
		typedValDst = &tTypedValue{&tType{person: "betty", age: 24}}
	)

	tcs := map[string]struct {
		flag *Rag
		exp  string
	}{
		"categorized bool": {
			&Rag{
				Name:     "flag-name",
				Category: "special bools",
				Usage:    "this is my special bool flag, note the category",
				Value:    &Bool{},
			},
			"\t   \t--flag-name\tthis is my special bool flag, note the category\t",
		},
		"categorized required bool": {
			&Rag{
				Name:     "flag-name",
				Category: "special bools",
				Required: true,
				Usage:    "this is my special bool flag, note the category",
				Value:    &Bool{},
			},
			"\t   \t--flag-name\t[required] this is my special bool flag, note the category\t",
		},
		"categorized required bool with shorthand": {
			&Rag{
				Name:     "flag-name",
				Category: "special bools",
				Required: true,
				Short:    "f",
				Usage:    "this is my special bool flag, note the category",
				Value:    &Bool{},
			},
			"\t-f,\t--flag-name\t[required] this is my special bool flag, note the category\t",
		},
		"default true bool": {
			&Rag{
				Name:     "flag-name",
				Category: "special bools",
				Required: true,
				Usage:    "this is my special bool flag, note the category",
				Value:    &Bool{Default: true},
			},
			"\t   \t--flag-name\t[required] this is my special bool flag, note the category [default: true]\t",
		},
		"categorized required var": {
			// Imitate what RagSet.Var does
			&Rag{
				Name:     "flag-name",
				Category: "special vars",
				Required: true,
				Usage:    "special var flag with category",
				Value:    valDst,
			},
			"\t   \t--flag-name value\t[required] special var flag with category [default: tom is 100 years old]\t",
		},
		"categorized typed var with shorthand": {
			// Imitate what RagSet.Var does
			&Rag{
				Name:     "flag-name",
				Category: "special vars",
				Usage:    "special var flag with category",
				Value:    typedValDst,
				Short:    "f",
			},
			"\t-f,\t--flag-name tvalue\tspecial var flag with category [default: betty is 24 years old]\t",
		},

		// TODO: categorized w shorthand, required w shorthand
		// TODO: string variations, int, duration, var what other kinds? depends on data type mappings
	}

	for name, tc := range tcs {
		tc := tc
		name := name
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			rs := New(name, flag.ContinueOnError, tc.flag)
			actual := usageLine(tc.flag, rs.FlagSet())
			assert.Equal(t, tc.exp, actual)
		})
	}
}

func TestHasZeroDefault(t *testing.T) {
	var (
		emptyVar  = &tTypedValue{new(tType)}
		customVar = &tTypedValue{&tType{person: "tom", age: 100}}
	)

	tcs := map[string]struct {
		v   flag.Getter
		exp bool
	}{
		"empty string": {&String{}, true},
		"string":       {&String{Default: "foo"}, false},
		"uint 0":       {&Uint{}, true},
		"uint":         {&Uint{Default: 10}, false},
		"uint8 0":      {&Uint8{}, true},
		"uint8":        {&Uint8{Default: 10}, false},
		"uint16 0":     {&Uint16{}, true},
		"uint16":       {&Uint16{Default: 10}, false},
		"uint32 0":     {&Uint32{}, true},
		"uint32":       {&Uint32{Default: 10}, false},
		"uint64 0":     {&Uint64{}, true},
		"uint64":       {&Uint64{Default: 10}, false},
		"int 0":        {&Int{}, true},
		"int":          {&Int{Default: 10}, false},
		"int8 0":       {&Int8{}, true},
		"int8":         {&Int8{Default: 10}, false},
		"int16 0":      {&Int16{}, true},
		"int16":        {&Int16{Default: 10}, false},
		"int32 0":      {&Int32{}, true},
		"int32":        {&Int32{Default: 10}, false},
		"int64 0":      {&Int64{}, true},
		"int64":        {&Int64{Default: 10}, false},
		"float32 0":    {&Float32{}, true},
		"float32":      {&Float32{Default: 10}, false},
		"float64 0":    {&Float64{}, true},
		"float64":      {&Float64{Default: 10}, false},
		"complex64 0":  {&Complex64{}, true},
		"complex64":    {&Complex64{Default: 10}, false},
		"complex128 0": {&Complex128{}, true},
		"complex128":   {&Complex128{Default: complex(3, -5)}, false},
		"var empty":    {emptyVar, true},
		"var":          {customVar, false},
	}

	for name, tc := range tcs {
		t.Run(name, func(t *testing.T) {
			rs := New(name, flag.ContinueOnError)
			r := &Rag{Name: strings.ReplaceAll(name, " ", "-"), Value: tc.v}
			assert.NotPanics(t, func() { rs.Add(r) })
			f := rs.FlagSet().Lookup(r.Name)
			assert.Equal(t, tc.exp, hasZeroValueDefault(f))
		})
	}
}

func TestPanicsWithBigShort(t *testing.T) {
	var (
		bigShort string
	)
	lsRag := &Rag{
		Name:     "big-short",
		Value:    &String{Var: &bigShort},
		Short:    "bs",
		Required: true,
	}

	assert.Panics(t, func() { New("bigShort Set", flag.ContinueOnError, lsRag) })
}

func TestUnquoteUsage(t *testing.T) {
	var (
		strDst     string
		int64Dst   int64
		intDst     int
		float64Dst float64
		boolDst    bool
		untypedDst = &tType{}
		typedDst   = &tTypedValue{new(tType)}
	)

	tcs := map[string]struct {
		usage    string
		val      flag.Getter
		expName  string
		expUsage string
	}{
		"backticked name": {
			"load configuration from `file`",
			&String{Default: "~/.config/file", Var: &strDst},
			"file", "load configuration from file",
		},
		"int64 is int": {
			"weird verbosity level",
			&Int64{Default: 100, Var: &int64Dst},
			"int", "weird verbosity level",
		},
		"int is int": {
			"weird verbosity level",
			&Int{Default: 100, Var: &intDst},
			"int", "weird verbosity level",
		},
		"float64 is float": {
			"weird verbosity level",
			&Float64{Default: 100, Var: &float64Dst},
			"float", "weird verbosity level",
		},
		"bool is empty": {
			"weird verbosity toggle",
			&Bool{Var: &boolDst},
			"", "weird verbosity toggle",
		},
		"backtick takes precedence": {
			"weird verbosity `level`",
			&Int64{Default: 100, Var: &int64Dst},
			"level", "weird verbosity level",
		},
		"implements TypedValue": {
			"weird verbosity level",
			typedDst,
			"tvalue", "weird verbosity level",
		},
		"fallback to value": {
			"weird verbosity level",
			untypedDst,
			"value", "weird verbosity level",
		},
		"single backtick is skipped": {
			"weird verbosity `level",
			&Int64{Default: 100, Var: &int64Dst},
			"int", "weird verbosity `level",
		},
	}

	for name, tc := range tcs {
		t.Run(name, func(t *testing.T) {
			f := &flag.Flag{Usage: tc.usage, Value: tc.val}
			varname, usage := UnquoteUsage(f)
			assert.Equal(t, tc.expName, varname)
			assert.Equal(t, tc.expUsage, usage)
		})
	}
}

func TestUsage(t *testing.T) {
	tcs := map[string]struct {
		rs  func() *RagSet
		exp string
	}{
		"simple": {
			func() *RagSet {
				rs := New("simple", flag.ContinueOnError)
				rs.Bool("force", false, "force operation in event of conflict")
				rs.String("target", "", "action target")
				rs.String("config", "", "config `file` to load", WithShort("c"))
				return rs
			},
			`Flags:
      --force         force operation in event of conflict 
      --target string action target                        
  -c, --config file   config file to load                  
`,
		},
		"categories": {
			func() *RagSet {
				rs := New("simple", flag.ContinueOnError)
				rs.Bool("force", false, "force operation in event of conflict")
				rs.String("target", "", "action target")
				rs.String("config", "", "config `file` to load", WithShort("c"))
				rs.Int("channel", 0, "slack channel ID to notify", WithCategory("slack"))
				rs.Bool("notify", false, "send a slack notification",
					WithShort("n"), WithCategory("slack"))
				return rs
			},
			`Flags:
      --force         force operation in event of conflict 
      --target string action target                        
  -c, --config file   config file to load                  

Slack Flags:
      --channel int slack channel ID to notify 
  -n, --notify      send a slack notification  
`,
		},
		"only categories": {
			func() *RagSet {
				rs := New("simple", flag.ContinueOnError)
				rs.Bool("force", false, "force operation in event of conflict", WithCategory("action"))
				rs.String("target", "", "action target", WithCategory("action"))
				rs.String("config", "", "config `file` to load", WithShort("c"), WithCategory("action"))
				rs.Int("channel", 0, "slack channel ID to notify", WithCategory("slack"))
				rs.Bool("notify", false, "send a slack notification",
					WithShort("n"), WithCategory("slack"))
				return rs
			},
			`Action Flags:
      --force         force operation in event of conflict 
      --target string action target                        
  -c, --config file   config file to load                  

Slack Flags:
      --channel int slack channel ID to notify 
  -n, --notify      send a slack notification  
`,
		},
		"categories without shorts": {
			func() *RagSet {
				rs := New("simple", flag.ContinueOnError)
				rs.Bool("force", false, "force operation in event of conflict")
				rs.String("target", "", "action target")
				rs.String("config", "", "config `file` to load", WithShort("c"))
				rs.Int("channel", 0, "slack channel ID to notify", WithCategory("slack"))
				rs.Bool("notify", false, "send a slack notification", WithCategory("slack"))
				return rs
			},
			`Flags:
      --force         force operation in event of conflict 
      --target string action target                        
  -c, --config file   config file to load                  

Slack Flags:
      --channel int slack channel ID to notify 
      --notify      send a slack notification  
`,
		},
	}

	for name, tc := range tcs {
		name := name
		tc := tc
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			// Run each test case multiple times to ensure output does not
			// shift.
			for i := 0; i < 5; i++ {
				b := new(bytes.Buffer)
				rs := tc.rs()
				rs.SetOutput(b)
				rs.Usage()
				assert.Equal(t, tc.exp, b.String())
			}
		})
	}
}