package filesystem

import (
	"io/fs"
	"os"
	"path/filepath"
	"strings"
	"testing"

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

func NewTestFileSystem() FileSystem {
	replaceffs := FileSystem{
		Stat: func(s string) (os.FileInfo, error) {
			if strings.HasSuffix(s, "existing_file.txt") {
				return fileStat{mode: 0}, nil
			}
			if strings.HasSuffix(s, "existing_dir") {
				return fileStat{mode: fs.ModeDir}, nil
			}
			return nil, os.ErrExist
		},
		Getwd: func() (string, error) {
			return "/test/dir", nil
		},
		EvalSymlinks: func(s string) (string, error) {
			if s == "/test/dir" {
				return "/real/dir", nil
			} else {
				if strings.Contains(s, "missing") {
					return "", os.ErrExist
				}
				return s, nil
			}
		},
	}
	return *FromFileSystem(replaceffs)
}

func TestFs_resolveSymlinks(t *testing.T) {
	cases := []struct {
		name        string
		symlinkPath string
		expected    string
		expectedErr error
	}{
		{
			name:        "existing file symlink",
			symlinkPath: "../existing_file.txt",
			expected:    "/real/existing_file.txt",
			expectedErr: nil,
		},
		{
			name:        "missing file symlink",
			symlinkPath: "../missing_file.txt",
			expected:    "",
			expectedErr: os.ErrExist,
		},
		{
			name:        "local existing file symlink",
			symlinkPath: "./existing_file.txt",
			expected:    "./existing_file.txt",
			expectedErr: nil,
		},
		{
			name:        "absolute existing file symlink",
			symlinkPath: "/a/b/c/existing_file.txt",
			expected:    "/a/b/c/existing_file.txt",
			expectedErr: nil,
		},
		{
			name:        "current directory existing file symlink",
			symlinkPath: "existing_file.txt",
			expected:    "existing_file.txt",
			expectedErr: nil,
		},
	}

	ffs := NewTestFileSystem()

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			path, err := ffs.resolveSymlinks(c.symlinkPath)
			if c.expectedErr != nil {
				require.ErrorIs(t, err, c.expectedErr)
			} else {
				require.NoError(t, err)
			}
			assert.Equal(t, c.expected, path)
		})
	}
}

func TestFs_fileExistsDefault(t *testing.T) {
	cases := []struct {
		name        string
		filePath    string
		expected    bool
		expectedErr error
	}{
		{
			name:        "existing file",
			filePath:    "existing_file.txt",
			expected:    true,
			expectedErr: nil,
		},
		{
			name:        "missing file",
			filePath:    "missing_file.txt",
			expected:    false,
			expectedErr: os.ErrExist,
		},
	}

	ffs := NewTestFileSystem()

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			exists, err := ffs.FileExists(c.filePath)
			if c.expectedErr != nil {
				require.ErrorIs(t, err, c.expectedErr)
			} else {
				require.NoError(t, err)
			}
			assert.Equal(t, c.expected, exists)
		})
	}
}

func TestFs_fileExistsAtDefault(t *testing.T) {
	cases := []struct {
		name     string
		filePath string
		expected bool
	}{
		{
			name:     "existing file",
			filePath: "existing_file.txt",
			expected: true,
		},
		{
			name:     "missing file",
			filePath: "missing_file.txt",
			expected: false,
		},
		{
			name:     "existing dir",
			filePath: "existing_dir",
			expected: false,
		},
	}

	ffs := NewTestFileSystem()

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			exists := ffs.FileExistsAt(c.filePath)
			assert.Equal(t, c.expected, exists)
		})
	}
}

func TestFs_directoryExistsDefault(t *testing.T) {
	cases := []struct {
		name     string
		dirPath  string
		expected bool
	}{
		{
			name:     "existing dir",
			dirPath:  "existing_dir",
			expected: true,
		},
		{
			name:     "missing dir",
			dirPath:  "missing_dir",
			expected: false,
		},
	}

	ffs := NewTestFileSystem()

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			exists := ffs.DirectoryExistsAt(c.dirPath)
			assert.Equal(t, c.expected, exists)
		})
	}
}

func TestFsTeadFile(t *testing.T) {
	cases := []struct {
		name      string
		content   []byte
		path      string
		wantError string
	}{
		{
			name:    "read file",
			content: []byte("hello helmfile"),
			path:    "helmfile.yaml",
		},
		{
			name:    "read file from stdin",
			content: []byte("hello helmfile"),
			path:    "-",
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			dir := t.TempDir()
			yamlPath := filepath.Join(dir, c.path)

			dfs := DefaultFileSystem()
			tmpfile, err := os.Create(yamlPath)
			require.NoErrorf(t, err, "create file %s error: %v", yamlPath, err)

			_, err = tmpfile.Write(c.content)
			require.NoErrorf(t, err, "write to file %s error: %v", yamlPath, err)

			readPath := yamlPath
			if c.path == "-" {
				readPath = c.path
				oldOsStdin := os.Stdin
				defer func() { os.Stdin = oldOsStdin }()
				os.Stdin = tmpfile
			}
			_, err = tmpfile.Seek(0, 0)
			require.NoErrorf(t, err, "file %s seek error: %v", yamlPath, err)

			want, err := dfs.readFile(readPath)
			require.NoErrorf(t, err, "read file %s error: %v", readPath, err)
			require.Equalf(t, string(c.content), string(want), "unexpected error: unexpected=%s, got=%v", string(c.content), string(want))
		})
	}
}

func TestFs_DefaultBuilder(t *testing.T) {
	ffs := DefaultFileSystem()
	assert.NotNil(t, ffs.ReadFile)
	assert.NotNil(t, ffs.ReadDir)
	assert.NotNil(t, ffs.DeleteFile)
	assert.NotNil(t, ffs.FileExists)
	assert.NotNil(t, ffs.Glob)
	assert.NotNil(t, ffs.FileExistsAt)
	assert.NotNil(t, ffs.DirectoryExistsAt)
	assert.NotNil(t, ffs.Stat)
	assert.NotNil(t, ffs.Getwd)
	assert.NotNil(t, ffs.Chdir)
	assert.NotNil(t, ffs.Abs)
	assert.NotNil(t, ffs.EvalSymlinks)
	assert.NotNil(t, ffs.WriteFile)
	assert.NotNil(t, ffs.MkdirTemp)
	assert.NotNil(t, ffs.RemoveAll)
	assert.NotNil(t, ffs.CopyDir)
}
