package cli

import (
	"fmt"
	"math/rand"
	"os"
	"path/filepath"
	"testing"

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

func TestZeroValueSourceChain(t *testing.T) {
	var vc ValueSourceChain
	assert.Empty(t, vc.EnvKeys())
	assert.NotEmpty(t, vc.GoString())
	assert.Empty(t, vc.Chain)
	assert.Empty(t, vc.String())
}

func TestEnvVarValueSource(t *testing.T) {
	t.Run("implements ValueSource", func(t *testing.T) {
		src := EnvVar("foo")
		require.Implements(t, (*ValueSource)(nil), src)

		t.Run("not found", func(t *testing.T) {
			t.Setenv("foo", "bar")

			src := EnvVar("foo_1")
			_, ok := src.Lookup()
			require.False(t, ok)
		})

		t.Run("found", func(t *testing.T) {
			t.Setenv("foo", "bar")

			r := require.New(t)
			src := EnvVar("foo")

			str, ok := src.Lookup()
			r.True(ok)
			r.Equal(str, "bar")
		})
	})

	t.Run("implements fmt.Stringer", func(t *testing.T) {
		src := EnvVar("foo")
		r := require.New(t)

		r.Implements((*fmt.Stringer)(nil), src)
		r.Equal("environment variable \"foo\"", src.String())
	})

	t.Run("implements fmt.GoStringer", func(t *testing.T) {
		src := EnvVar("foo")
		r := require.New(t)

		r.Implements((*fmt.GoStringer)(nil), src)
		r.Equal("&envVarValueSource{Key:\"foo\"}", src.GoString())
	})
}

func TestEnvVars(t *testing.T) {
	t.Setenv("myfoo", "mybar")

	source := EnvVars("foo1", "myfoo")
	str, src, ok := source.LookupWithSource()

	r := require.New(t)
	r.True(ok)
	r.Equal(str, "mybar")
	r.Contains(src.String(), "\"myfoo\"")
}

func TestFileValueSource(t *testing.T) {
	t.Run("implements ValueSource", func(t *testing.T) {
		r := require.New(t)

		r.Implements((*ValueSource)(nil), &fileValueSource{})

		t.Run("not found", func(t *testing.T) {
			src := File(fmt.Sprintf("junk_file_name-%[1]v", rand.Int()))
			_, ok := src.Lookup()
			r.False(ok)
		})

		fileName := filepath.Join(os.TempDir(), fmt.Sprintf("urfave-cli-testing-existing_file-%[1]v", rand.Int()))
		t.Cleanup(func() { _ = os.Remove(fileName) })

		r.Nil(os.WriteFile(fileName, []byte("pita"), 0o644))

		t.Run("found", func(t *testing.T) {
			src := File(fileName)
			str, ok := src.Lookup()
			r.True(ok)
			r.Equal("pita", str)
		})
	})

	t.Run("implements fmt.Stringer", func(t *testing.T) {
		src := File("/dev/null")
		r := require.New(t)

		r.Implements((*ValueSource)(nil), src)
		r.Equal("file \"/dev/null\"", src.String())
	})

	t.Run("implements fmt.GoStringer", func(t *testing.T) {
		src := File("/dev/null")
		r := require.New(t)

		r.Implements((*ValueSource)(nil), src)
		r.Equal("&fileValueSource{Path:\"/dev/null\"}", src.GoString())
	})
}

func TestFilePaths(t *testing.T) {
	r := require.New(t)

	fileName := filepath.Join(os.TempDir(), fmt.Sprintf("urfave-cli-tests-some_file_name_%[1]v", rand.Int()))
	t.Cleanup(func() { _ = os.Remove(fileName) })

	r.Nil(os.WriteFile(fileName, []byte("Hello"), 0o644))

	sources := Files("junk_file_name", fileName)
	str, src, ok := sources.LookupWithSource()
	r.True(ok)
	r.Equal(str, "Hello")
	r.Contains(src.String(), fmt.Sprintf("%[1]q", fileName))
}

func TestValueSourceChainEnvKeys(t *testing.T) {
	chain := NewValueSourceChain(
		&staticValueSource{"hello"},
	)
	chain.Append(EnvVars("foo", "bar"))

	r := require.New(t)
	r.Equal([]string{"foo", "bar"}, chain.EnvKeys())
}

func TestValueSourceChain(t *testing.T) {
	t.Run("implements ValueSource", func(t *testing.T) {
		vsc := &ValueSourceChain{}
		r := require.New(t)

		r.Implements((*ValueSource)(nil), vsc)

		_, ok := vsc.Lookup()
		r.False(ok)
	})

	t.Run("implements fmt.GoStringer", func(t *testing.T) {
		vsc := &ValueSourceChain{}
		r := require.New(t)

		r.Implements((*fmt.GoStringer)(nil), vsc)
		r.Equal("&ValueSourceChain{Chain:{}}", vsc.GoString())

		vsc1 := NewValueSourceChain(&staticValueSource{v: "yahtzee"},
			&staticValueSource{v: "matzoh"},
		)
		r.Equal("&ValueSourceChain{Chain:{&staticValueSource{v:\"yahtzee\"},&staticValueSource{v:\"matzoh\"}}}", vsc1.GoString())
	})

	t.Run("implements fmt.Stringer", func(t *testing.T) {
		vsc := &ValueSourceChain{}
		r := require.New(t)

		r.Implements((*fmt.Stringer)(nil), vsc)
		r.Equal("", vsc.String())

		vsc1 := NewValueSourceChain(
			&staticValueSource{v: "soup"},
			&staticValueSource{v: "salad"},
			&staticValueSource{v: "pumpkins"},
		)
		r.Equal("soup,salad,pumpkins", vsc1.String())
	})
}

type staticValueSource struct {
	v string
}

func (svs *staticValueSource) GoString() string {
	return fmt.Sprintf("&staticValueSource{v:%[1]q}", svs.v)
}
func (svs *staticValueSource) String() string         { return svs.v }
func (svs *staticValueSource) Lookup() (string, bool) { return svs.v, true }

func TestMapValueSource(t *testing.T) {
	tests := []struct {
		name  string
		m     map[any]any
		key   string
		val   string
		found bool
	}{
		{
			name: "No map no key",
		},
		{
			name: "No map with key",
			key:  "foo",
		},
		{
			name: "Empty map no key",
			m:    map[any]any{},
		},
		{
			name: "Empty map with key",
			key:  "foo",
			m:    map[any]any{},
		},
		{
			name: "Level 1 no key",
			key:  ".foob",
			m: map[any]any{
				"foo": 10,
			},
		},
		{
			name: "Level 1",
			key:  "foobar",
			m: map[any]any{
				"foobar": 10,
			},
			val:   "10",
			found: true,
		},
		{
			name: "Level 2",
			key:  "foo.bar",
			m: map[any]any{
				"foo": map[any]any{
					"bar": 10,
				},
			},
			val:   "10",
			found: true,
		},
		{
			name: "Level 2 invalid key",
			key:  "foo.bar1",
			m: map[any]any{
				"foo": map[any]any{
					"bar": "10",
				},
			},
		},
		{
			name: "Level 2 string map type",
			key:  "foo.bar1",
			m: map[any]any{
				"foo": map[string]any{
					"bar": "10",
				},
			},
		},
		{
			name: "Level 3 no entry",
			key:  "foo.bar.t",
			m: map[any]any{
				"foo": map[any]any{
					"bar": "sss",
				},
			},
		},
		{
			name: "Level 3",
			key:  "foo.bar.t",
			m: map[any]any{
				"foo": map[any]any{
					"bar": map[any]any{
						"t": "sss",
					},
				},
			},
			val:   "sss",
			found: true,
		},
		{
			name: "Level 3 invalid key",
			key:  "foo.bar.t",
			m: map[any]any{
				"foo": map[any]any{
					"bar": map[any]any{
						"t1": 10,
					},
				},
			},
		},
		{
			name: "Level 4 no entry",
			key:  "foo.bar.t.gh",
			m: map[any]any{
				"foo": map[any]any{
					"bar": map[any]any{
						"t1": 10,
					},
				},
			},
		},
		{
			name: "Level 4 slice entry",
			key:  "foo.bar.t.gh",
			m: map[any]any{
				"foo": map[any]any{
					"bar": map[string]any{
						"t": map[any]any{
							"gh": []int{10},
						},
					},
				},
			},
			val:   "[10]",
			found: true,
		},
	}

	for _, test := range tests {
		t.Run(test.key, func(t *testing.T) {
			ms := NewMapSource("test", test.m)
			m := NewMapValueSource(test.key, ms)
			val, b := m.Lookup()
			if !test.found {
				assert.False(t, b)
			} else {
				assert.True(t, b)
				assert.Equal(t, val, test.val)
			}
		})
	}
}

func TestMapValueSourceStringer(t *testing.T) {
	m := map[any]any{
		"foo": map[any]any{
			"bar": 10,
		},
	}
	mvs := NewMapValueSource("bar", NewMapSource("test", m))

	assert.Equal(t, `&mapValueSource{key:"bar", src:&mapSource{name:"test"}}`, mvs.GoString())
	assert.Equal(t, `key "bar" from map source "test"`, mvs.String())
}
