package testhelper

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"sync"

	ffs "github.com/helmfile/helmfile/pkg/filesystem"
)

type TestFs struct {
	Cwd   string
	dirs  map[string]bool
	files map[string]string

	GlobFixtures map[string][]string
	DeleteFile   func(string) error

	ChdirCalls      int
	mu              sync.Mutex
	fileReaderCalls int
	successfulReads []string
}

func NewTestFs(files map[string]string) *TestFs {
	dirs := map[string]bool{}
	for abs := range files {
		for d := filepath.ToSlash(filepath.Dir(abs)); !dirs[d]; d = filepath.ToSlash(filepath.Dir(d)) {
			dirs[d] = true
		}
	}
	return &TestFs{
		Cwd:   "/path/to",
		dirs:  dirs,
		files: files,

		successfulReads: []string{},

		GlobFixtures: map[string][]string{},
		DeleteFile:   func(string) (ret error) { return },
	}
}

func (f *TestFs) ToFileSystem() *ffs.FileSystem {
	curfs := ffs.FileSystem{
		FileExistsAt:      f.FileExistsAt,
		FileExists:        f.FileExists,
		DirectoryExistsAt: f.DirectoryExistsAt,
		ReadFile:          f.ReadFile,
		Glob:              f.Glob,
		Getwd:             f.Getwd,
		Chdir:             f.Chdir,
		Abs:               f.Abs,
		DeleteFile:        f.DeleteFile,
	}
	trfs := ffs.FromFileSystem(curfs)
	return trfs
}

func (f *TestFs) FileExistsAt(path string) bool {
	var ok bool
	if strings.HasPrefix(path, "/") {
		_, ok = f.files[path]
	} else {
		_, ok = f.files[filepath.ToSlash(filepath.Join(f.Cwd, path))]
	}
	return ok
}

func (f *TestFs) FileExists(path string) (bool, error) {
	return f.FileExistsAt(path), nil
}

func (f *TestFs) DirectoryExistsAt(path string) bool {
	var ok bool
	if strings.HasPrefix(path, "/") {
		_, ok = f.dirs[path]
	} else {
		_, ok = f.dirs[filepath.ToSlash(filepath.Join(f.Cwd, path))]
	}
	return ok
}

func (f *TestFs) ReadFile(filename string) ([]byte, error) {
	var str string
	var ok bool
	if strings.HasPrefix(filename, "/") {
		str, ok = f.files[filename]
	} else {
		str, ok = f.files[filepath.ToSlash(filepath.Join(f.Cwd, filename))]
	}
	if !ok {
		return []byte(nil), os.ErrNotExist
	}

	f.mu.Lock()
	f.fileReaderCalls++
	f.successfulReads = append(f.successfulReads, filename)
	f.mu.Unlock()

	return []byte(str), nil
}

func (f *TestFs) SuccessfulReads() []string {
	f.mu.Lock()
	defer f.mu.Unlock()
	// Return a copy to avoid race conditions with callers
	result := make([]string, len(f.successfulReads))
	copy(result, f.successfulReads)
	return result
}

func (f *TestFs) FileReaderCalls() int {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.fileReaderCalls
}

func (f *TestFs) Glob(relPattern string) ([]string, error) {
	var pattern string
	if strings.HasPrefix(relPattern, "/") {
		pattern = relPattern
	} else {
		pattern = filepath.ToSlash(filepath.Join(f.Cwd, relPattern))
	}

	fixtures, ok := f.GlobFixtures[pattern]
	if ok {
		return fixtures, nil
	}

	matches := []string{}
	for name := range f.files {
		matched, err := filepath.Match(pattern, name)
		if err != nil {
			return nil, err
		}
		if matched {
			matches = append(matches, name)
		}
	}
	return matches, nil
}

func (f *TestFs) Abs(path string) (string, error) {
	path = filepath.ToSlash(path)
	var p string
	if strings.HasPrefix(path, "/") {
		p = path
	} else {
		p = filepath.Join(f.Cwd, path)
	}
	return filepath.ToSlash(filepath.Clean(p)), nil
}

func (f *TestFs) Getwd() (string, error) {
	return f.Cwd, nil
}

func (f *TestFs) Chdir(dir string) error {
	f.mu.Lock()
	f.ChdirCalls++
	f.mu.Unlock()
	if _, ok := f.dirs[dir]; ok {
		f.Cwd = dir
		return nil
	}
	return fmt.Errorf("unexpected chdir \"%s\"", dir)
}

// SyncWriter wraps an io.Writer to make it safe for concurrent use.
type SyncWriter struct {
	mu sync.Mutex
	w  io.Writer
}

// NewSyncWriter creates a new thread-safe writer.
func NewSyncWriter(w io.Writer) *SyncWriter {
	return &SyncWriter{w: w}
}

// Write implements io.Writer in a thread-safe manner.
func (sw *SyncWriter) Write(p []byte) (n int, err error) {
	sw.mu.Lock()
	defer sw.mu.Unlock()
	return sw.w.Write(p)
}
