package altsrc_test

import (
	"errors"
	"fmt"
	"log"
	"os"
	"testing"
	"time"

	"github.com/urfave/cli/v2"
	"github.com/urfave/cli/v2/altsrc"
)

func ExampleApp_Run_yamlFileLoaderDuration() {
	execServe := func(c *cli.Context) error {
		keepaliveInterval := c.Duration("keepalive-interval")
		fmt.Printf("keepalive %s\n", keepaliveInterval)
		return nil
	}

	fileExists := func(filename string) bool {
		stat, _ := os.Stat(filename)
		return stat != nil
	}

	// initConfigFileInputSource is like InitInputSourceWithContext and NewYamlSourceFromFlagFunc, but checks
	// if the config flag is exists and only loads it if it does. If the flag is set and the file exists, it fails.
	initConfigFileInputSource := func(configFlag string, flags []cli.Flag) cli.BeforeFunc {
		return func(context *cli.Context) error {
			configFile := context.String(configFlag)
			if context.IsSet(configFlag) && !fileExists(configFile) {
				return fmt.Errorf("config file %s does not exist", configFile)
			} else if !context.IsSet(configFlag) && !fileExists(configFile) {
				return nil
			}
			inputSource, err := altsrc.NewYamlSourceFromFile(configFile)
			if err != nil {
				return err
			}
			return altsrc.ApplyInputSourceValues(context, inputSource, flags)
		}
	}

	flagsServe := []cli.Flag{
		&cli.StringFlag{
			Name:        "config",
			Aliases:     []string{"c"},
			EnvVars:     []string{"CONFIG_FILE"},
			Value:       "../testdata/empty.yml",
			DefaultText: "../testdata/empty.yml",
			Usage:       "config file",
		},
		altsrc.NewDurationFlag(
			&cli.DurationFlag{
				Name:    "keepalive-interval",
				Aliases: []string{"k"},
				EnvVars: []string{"KEEPALIVE_INTERVAL"},
				Value:   45 * time.Second,
				Usage:   "interval of keepalive messages",
			},
		),
	}

	cmdServe := &cli.Command{
		Name:      "serve",
		Usage:     "Run the server",
		UsageText: "serve [OPTIONS..]",
		Action:    execServe,
		Flags:     flagsServe,
		Before:    initConfigFileInputSource("config", flagsServe),
	}

	c := &cli.App{
		Name:                   "cmd",
		HideVersion:            true,
		UseShortOptionHandling: true,
		Commands: []*cli.Command{
			cmdServe,
		},
	}

	if err := c.Run([]string{"cmd", "serve", "--config", "../testdata/empty.yml"}); err != nil {
		log.Fatal(err)
	}

	// Output:
	// keepalive 45s
}

func TestYamlFileInt64Slice(t *testing.T) {
	_ = os.WriteFile("current.yaml", []byte(`top:
  test: [100, 9223372036854775808]`), 0666)
	defer os.Remove("current.yaml")

	testFlag := []cli.Flag{
		&altsrc.StringFlag{StringFlag: &cli.StringFlag{Name: "conf"}},
		&altsrc.Int64SliceFlag{Int64SliceFlag: &cli.Int64SliceFlag{Name: "top.test"}},
	}
	app := &cli.App{}
	app.Before = altsrc.InitInputSourceWithContext(testFlag, altsrc.NewYamlSourceFromFlagFunc("conf"))
	app.Action = func(c *cli.Context) error { return nil }
	app.Flags = append(app.Flags, testFlag...)

	test := []string{"testApp", "--conf", "current.yaml"}
	if err := app.Run(test); err == nil {
		t.Error("should return the mismatch error")
	}
}

func TestYamlFileStringSlice(t *testing.T) {
	_ = os.WriteFile("current.yaml", []byte(`top:
  test: ["s1", "s2"]`), 0666)
	defer os.Remove("current.yaml")

	testFlag := []cli.Flag{
		&altsrc.StringFlag{StringFlag: &cli.StringFlag{Name: "conf"}},
		&altsrc.StringSliceFlag{StringSliceFlag: &cli.StringSliceFlag{Name: "top.test", EnvVars: []string{"THE_TEST"}}},
	}
	app := &cli.App{}
	app.Before = altsrc.InitInputSourceWithContext(testFlag, altsrc.NewYamlSourceFromFlagFunc("conf"))
	app.Action = func(c *cli.Context) error {
		if c.IsSet("top.test") {
			return nil
		} else {
			return errors.New("top.test is not set")
		}
	}
	app.Flags = append(app.Flags, testFlag...)

	test := []string{"testApp", "--conf", "current.yaml"}
	if err := app.Run(test); err != nil {
		t.Error(err)
	}
}

func TestYamlFileUint64(t *testing.T) {
	tests := []struct {
		name  string
		entry string
		err   bool
	}{
		{
			"top.test",
			`top:
  test: 100`,
			false,
		},
		{
			"test",
			"test: ",
			false,
		},
		{
			"test",
			"test: 100", //int
			false,
		},
		{
			"test",
			"test: -100", //int
			true,
		},
		{
			"test",
			"test: 9223372036854775807", //int
			false,
		},
		{
			"test",
			"test: 9223372036854775808", //uint64
			false,
		},
		{
			"test",
			"test: 19223372036854775808", //float64
			true,
		},
	}

	for i, test := range tests {
		_ = os.WriteFile("current.yaml", []byte(test.entry), 0666)
		defer os.Remove("current.yaml")

		testFlag := []cli.Flag{
			&altsrc.StringFlag{StringFlag: &cli.StringFlag{Name: "conf"}},
			&altsrc.Uint64Flag{Uint64Flag: &cli.Uint64Flag{Name: test.name}},
		}
		app := &cli.App{}
		app.Flags = append(app.Flags, testFlag...)
		app.Before = altsrc.InitInputSourceWithContext(testFlag, altsrc.NewYamlSourceFromFlagFunc("conf"))

		appCmd := []string{"testApp", "--conf", "current.yaml"}
		err := app.Run(appCmd)
		if result := err != nil; result != test.err {
			t.Error(i, "testcast: expect error but", err)
		}
	}
}

func TestYamlFileUint(t *testing.T) {
	tests := []struct {
		name  string
		entry string
		err   bool
	}{
		{
			"top.test",
			`top:
  test: 100`,
			false,
		},
		{
			"test",
			"test: ",
			false,
		},
		{
			"test",
			"test: 100", //int
			false,
		},
		{
			"test",
			"test: -100", //int
			true,
		},
		{
			"test",
			"test: 775807", //int
			false,
		},
		{
			"test",
			"test: 4775808", //uint64
			false,
		},
		{
			"test",
			"test: 19223372036854775808", //float64
			true,
		},
	}

	for i, test := range tests {
		_ = os.WriteFile("current.yaml", []byte(test.entry), 0666)
		defer os.Remove("current.yaml")

		testFlag := []cli.Flag{
			&altsrc.StringFlag{StringFlag: &cli.StringFlag{Name: "conf"}},
			&altsrc.UintFlag{UintFlag: &cli.UintFlag{Name: test.name}},
		}
		app := &cli.App{}
		app.Flags = append(app.Flags, testFlag...)
		app.Before = altsrc.InitInputSourceWithContext(testFlag, altsrc.NewYamlSourceFromFlagFunc("conf"))

		appCmd := []string{"testApp", "--conf", "current.yaml"}
		err := app.Run(appCmd)
		if result := err != nil; result != test.err {
			t.Error(i, "testcast: expect error but", err)
		}
	}
}

func TestYamlFileInt64(t *testing.T) {
	tests := []struct {
		name  string
		entry string
		err   bool
	}{
		{
			"top.test",
			`top:
  test: 100`,
			false,
		},
		{
			"test",
			"test: ",
			false,
		},
		{
			"test",
			"test: 100", //int
			false,
		},
		{
			"test",
			"test: -100", //int
			false,
		},
		{
			"test",
			"test: 9223372036854775807", //int
			false,
		},
		{
			"test",
			"test: 9223372036854775808", //uint64
			true,
		},
		{
			"test",
			"test: 19223372036854775808", //float64
			true,
		},
	}

	for i, test := range tests {
		_ = os.WriteFile("current.yaml", []byte(test.entry), 0666)
		defer os.Remove("current.yaml")

		testFlag := []cli.Flag{
			&altsrc.StringFlag{StringFlag: &cli.StringFlag{Name: "conf"}},
			&altsrc.Int64Flag{Int64Flag: &cli.Int64Flag{Name: test.name}},
		}
		app := &cli.App{}
		app.Flags = append(app.Flags, testFlag...)
		app.Before = altsrc.InitInputSourceWithContext(testFlag, altsrc.NewYamlSourceFromFlagFunc("conf"))

		appCmd := []string{"testApp", "--conf", "current.yaml"}
		err := app.Run(appCmd)
		if result := err != nil; result != test.err {
			t.Error(i, "testcast: expect error but", err)
		}
	}
}
