package filesystem

import (
	"context"
	"errors"
	"os"
	"testing"
	"time"

	"github.com/alicebob/miniredis/v2"
	"github.com/valkey-io/valkey-go"
)

// MockFS implements filesystem.FS interface for testing.
type MockFS struct {
	files map[string]*MockFile
}

// MockFile implements FileInfo and holds content.
type MockFile struct {
	name        string
	content     []byte
	mode        os.FileMode
	modTime     time.Time
	isDir       bool
	contentType string
	path        string
}

func (f *MockFile) Name() string        { return f.name }
func (f *MockFile) Size() int64         { return int64(len(f.content)) }
func (f *MockFile) Mode() os.FileMode   { return f.mode }
func (f *MockFile) ModTime() time.Time  { return f.modTime }
func (f *MockFile) IsDir() bool         { return f.isDir }
func (f *MockFile) Sys() any            { return nil }
func (f *MockFile) ContentType() string { return f.contentType }
func (f *MockFile) Path() string        { return f.path }

func NewMockFS() *MockFS {
	return &MockFS{
		files: make(map[string]*MockFile),
	}
}

func (m *MockFS) AddFile(path string, content []byte) {
	m.files[path] = &MockFile{
		name:        path,
		content:     content,
		mode:        0o644,
		modTime:     time.Now().Truncate(time.Second),
		isDir:       false,
		contentType: "text/plain",
		path:        path,
	}
}

func (m *MockFS) AddDir(path string) {
	m.files[path] = &MockFile{
		name:    path,
		mode:    0o755,
		modTime: time.Now().Truncate(time.Second),
		isDir:   true,
		path:    path,
	}
}

func (m *MockFS) Read(path string) ([]byte, error) {
	f, ok := m.files[path]
	if !ok {
		return nil, os.ErrNotExist
	}
	if f.isDir {
		return nil, errors.New("is a directory")
	}
	return f.content, nil
}

func (m *MockFS) ReadDir(path string) ([]os.FileInfo, error) {
	// Simple implementation: return files that start with path + "/"
	// For deeper nesting, simulate direct children only.
	// Assume flat structure for simplicity or specific test setup.
	var infos []os.FileInfo
	for p, f := range m.files {
		// Just check if it's logically "inside" the directory
		// e.g. path="/foo", file="/foo/bar" -> yes
		// file="/foo/bar/baz" -> no (if strict direct children)
		// file="/other" -> no
		if p == path {
			continue // self
		}
		// Check prefix
		// if path is root "/", check if p has no other slashes?
		// if path is "/foo", check if p starts with "/foo/" and has no further slashes
		// This logic is simple but sufficient for tests.
		// Let's assume absolute paths.
		// If path does not end with /, append it for prefix check
		prefix := path
		if len(prefix) > 0 && prefix[len(prefix)-1] != '/' {
			prefix += "/"
		}
		
		if len(p) > len(prefix) && p[:len(prefix)] == prefix {
			// Ensure direct child (no more slashes)
			rel := p[len(prefix):]
			// Count slashes in rel. If 0, it's a direct child.
			slashCount := 0
			for _, c := range rel {
				if c == '/' {
					slashCount++
				}
			}
			if slashCount == 0 {
				infos = append(infos, f)
			}
		}
	}
	if len(infos) == 0 {
		// Check if directory exists at all
		if _, ok := m.files[path]; !ok {
			return nil, os.ErrNotExist
		}
	}
	return infos, nil
}

func (m *MockFS) Stat(path string) (os.FileInfo, error) {
	f, ok := m.files[path]
	if !ok {
		return nil, os.ErrNotExist
	}
	return f, nil
}

func TestCachedClient_Stat(t *testing.T) {
	mr, err := miniredis.Run()
	if err != nil {
		t.Fatalf("failed to start miniredis: %v", err)
	}
	defer mr.Close()

	client, err := valkey.NewClient(valkey.ClientOption{
		InitAddress:  []string{mr.Addr()},
		DisableCache: true,
	})
	if err != nil {
		t.Fatalf("failed to create valkey client: %v", err)
	}
	defer client.Close()

	mockFS := NewMockFS()
	mockFS.AddFile("/test.txt", []byte("hello"))

	c := NewCachedClient(mockFS, client, time.Hour)

	// First call: Cache miss
	info, err := c.Stat("/test.txt")
	if err != nil {
		t.Fatalf("Stat failed: %v", err)
	}
	if info.Name() != "/test.txt" {
		t.Errorf("expected name /test.txt, got %s", info.Name())
	}
	if c.misses.Load() != 1 {
		t.Errorf("expected 1 miss, got %d", c.misses.Load())
	}
	if c.hits.Load() != 0 {
		t.Errorf("expected 0 hits, got %d", c.hits.Load())
	}

	// Second call: Cache hit
	info2, err := c.Stat("/test.txt")
	if err != nil {
		t.Fatalf("Stat failed: %v", err)
	}
	if info2.Size() != info.Size() {
		t.Errorf("size mismatch")
	}
	if c.misses.Load() != 1 {
		t.Errorf("expected 1 miss, got %d", c.misses.Load())
	}
	if c.hits.Load() != 1 {
		t.Errorf("expected 1 hit, got %d", c.hits.Load())
	}

	// Verify valkey content
	keys := mr.Keys()
	if len(keys) != 1 {
		t.Errorf("expected 1 key in redis, got %d", len(keys))
	}
}

func TestCachedClient_Read(t *testing.T) {
	mr, err := miniredis.Run()
	if err != nil {
		t.Fatalf("failed to start miniredis: %v", err)
	}
	defer mr.Close()

	client, err := valkey.NewClient(valkey.ClientOption{
		InitAddress:  []string{mr.Addr()},
		DisableCache: true,
	})
	if err != nil {
		t.Fatalf("failed to create valkey client: %v", err)
	}
	defer client.Close()

	mockFS := NewMockFS()
	content := []byte("hello world")
	mockFS.AddFile("/read.txt", content)

	c := NewCachedClient(mockFS, client, time.Hour)

	// First call: Miss
	data, err := c.Read("/read.txt")
	if err != nil {
		t.Fatalf("Read failed: %v", err)
	}
	if string(data) != string(content) {
		t.Errorf("content mismatch")
	}
	if c.misses.Load() != 1 {
		t.Errorf("expected 1 miss, got %d", c.misses.Load())
	}

	// Second call: Hit
	data2, err := c.Read("/read.txt")
	if err != nil {
		t.Fatalf("Read failed: %v", err)
	}
	if string(data2) != string(content) {
		t.Errorf("content mismatch")
	}
	if c.hits.Load() != 1 {
		t.Errorf("expected 1 hit, got %d", c.hits.Load())
	}
}

func TestCachedClient_ReadDir(t *testing.T) {
	mr, err := miniredis.Run()
	if err != nil {
		t.Fatalf("failed to start miniredis: %v", err)
	}
	defer mr.Close()

	client, err := valkey.NewClient(valkey.ClientOption{
		InitAddress:  []string{mr.Addr()},
		DisableCache: true,
	})
	if err != nil {
		t.Fatalf("failed to create valkey client: %v", err)
	}
	defer client.Close()

	mockFS := NewMockFS()
	mockFS.AddDir("/dir")
	mockFS.AddFile("/dir/f1.txt", []byte("1"))
	mockFS.AddFile("/dir/f2.txt", []byte("2"))

	c := NewCachedClient(mockFS, client, time.Hour)

	// First call: Miss
	infos, err := c.ReadDir("/dir")
	if err != nil {
		t.Fatalf("ReadDir failed: %v", err)
	}
	if len(infos) != 2 {
		t.Errorf("expected 2 files, got %d", len(infos))
	}
	if c.misses.Load() != 1 {
		t.Errorf("expected 1 miss, got %d", c.misses.Load())
	}

	// Second call: Hit
	infos2, err := c.ReadDir("/dir")
	if err != nil {
		t.Fatalf("ReadDir failed: %v", err)
	}
	if len(infos2) != 2 {
		t.Errorf("expected 2 files, got %d", len(infos2))
	}
	if c.hits.Load() != 1 {
		t.Errorf("expected 1 hit, got %d", c.hits.Load())
	}
}

func TestCachedClient_RevalidateStat(t *testing.T) {
	mr, err := miniredis.Run()
	if err != nil {
		t.Fatalf("failed to start miniredis: %v", err)
	}
	defer mr.Close()

	client, err := valkey.NewClient(valkey.ClientOption{
		InitAddress:  []string{mr.Addr()},
		DisableCache: true,
	})
	if err != nil {
		t.Fatalf("failed to create valkey client: %v", err)
	}
	defer client.Close()

	mockFS := NewMockFS()
	mockFS.AddFile("/reval.txt", []byte("initial"))

	c := NewCachedClient(mockFS, client, time.Hour)

	// Populate cache
	_, _ = c.Stat("/reval.txt")

	// Update mock file (simulate external change)
	// For simplicity, just update size
	f := mockFS.files["/reval.txt"]
	f.content = []byte("updated content")
	// Important: update modTime to be newer than cached
	f.modTime = time.Now().Add(time.Minute)

	// Run revalidation manually
	// Note: revalidateStat is a private method, so we can test it directly here
	// because we are in package filesystem (if using package filesystem_test we couldn't)
	// But `revalidateStat` only iterates over keys in redis. So we need to ensure the key is there.
	// `Stat` put it there.

	// Wait, Stat puts it there. `revalidateStat` iterates keys matching `keyStat*`.
	// For each key, it checks `Stat` again.
	// Wait, looking at `revalidateStat` implementation:
	// It calls `c.impl.Stat(path)`. If successful, it updates cache.
	// It doesn't check ModTime explicitly for `Stat` revalidation, it just blindly updates?
	// Let's check `revalidateStat` implementation in `cache.go`.

	// func (c *CachedClient) revalidateStat(ctx context.Context) { ...
	//   info, err := c.impl.Stat(path)
	//   ...
	//   bytes, err := json.Marshal(f)
	//   c.client.Do(...Set...)
	// }
	// Yes, it blindly updates. So modification time doesn't matter for `Stat`, it just fetches fresh info.

	c.revalidateStat(context.Background())

	// Verify cache is updated by fetching from cache again
	// We can inspect redis directly or trust `Stat` returns cached value (which should be updated).
	// Let's inspect redis to be sure.
	// Or just call Stat again. It should be a cache HIT, but return NEW values.
	
	// Reset stats to check hit
	c.hits.Store(0)
	c.misses.Store(0)

	info, err := c.Stat("/reval.txt")
	if err != nil {
		t.Fatalf("Stat failed: %v", err)
	}
	
	if info.Size() != int64(len("updated content")) {
		t.Errorf("expected size %d, got %d", len("updated content"), info.Size())
	}
	
	// Should be a hit because revalidate updated the cache entry, so it exists and is valid.
	if c.hits.Load() != 1 {
		t.Errorf("expected 1 hit, got %d", c.hits.Load())
	}
}

func TestCachedClient_RevalidateRead(t *testing.T) {
	mr, err := miniredis.Run()
	if err != nil {
		t.Fatalf("failed to start miniredis: %v", err)
	}
	defer mr.Close()

	client, err := valkey.NewClient(valkey.ClientOption{
		InitAddress:  []string{mr.Addr()},
		DisableCache: true,
	})
	if err != nil {
		t.Fatalf("failed to create valkey client: %v", err)
	}
	defer client.Close()

	mockFS := NewMockFS()
	mockFS.AddFile("/read_reval.txt", []byte("initial"))

	c := NewCachedClient(mockFS, client, time.Hour)

	// Populate cache
	_, _ = c.Read("/read_reval.txt")

	// Update mock file (simulate external change)
	f := mockFS.files["/read_reval.txt"]
	f.content = []byte("updated content")
	f.modTime = time.Now().Add(time.Minute) // Newer ModTime triggers update

	// Run revalidation manually
	c.revalidateRead(context.Background())

	// Verify cache is updated
	// Reset stats
	c.hits.Store(0)
	c.misses.Store(0)

	data, err := c.Read("/read_reval.txt")
	if err != nil {
		t.Fatalf("Read failed: %v", err)
	}
	if string(data) != "updated content" {
		t.Errorf("expected updated content, got %s", string(data))
	}
	if c.hits.Load() != 1 {
		t.Errorf("expected 1 hit, got %d", c.hits.Load())
	}
}

func TestCachedClient_RevalidateReadDir(t *testing.T) {
	mr, err := miniredis.Run()
	if err != nil {
		t.Fatalf("failed to start miniredis: %v", err)
	}
	defer mr.Close()

	client, err := valkey.NewClient(valkey.ClientOption{
		InitAddress:  []string{mr.Addr()},
		DisableCache: true,
	})
	if err != nil {
		t.Fatalf("failed to create valkey client: %v", err)
	}
	defer client.Close()

	mockFS := NewMockFS()
	mockFS.AddDir("/dir_reval")
	mockFS.AddFile("/dir_reval/f1.txt", []byte("1"))

	c := NewCachedClient(mockFS, client, time.Hour)

	// Populate cache
	_, err = c.ReadDir("/dir_reval")
	if err != nil {
		t.Fatalf("ReadDir failed: %v", err)
	}

	// Update mock file (simulate external change)
	mockFS.AddFile("/dir_reval/f2.txt", []byte("2"))

	// Run revalidation manually
	c.revalidateReadDir(context.Background())

	// Verify cache is updated
	// Reset stats
	c.hits.Store(0)
	c.misses.Store(0)

	infos2, err := c.ReadDir("/dir_reval")
	if err != nil {
		t.Fatalf("ReadDir failed: %v", err)
	}
	if len(infos2) != 2 {
		t.Errorf("expected 2 files after revalidation, got %d", len(infos2))
	}
	if c.hits.Load() != 1 {
		t.Errorf("expected 1 hit, got %d", c.hits.Load())
	}
}

