package readonlyfs

import (
	"errors"
	"io"
	"io/fs"
	"testing"
	"testing/fstest"
)

func TestNewPassesFSTest(t *testing.T) {
	for _, test := range []struct {
		name  string
		files map[string][]byte
		paths []string
	}{
		{name: "empty", files: map[string][]byte{}, paths: nil},
		{name: "single file", files: map[string][]byte{"message.txt": []byte("hello")}, paths: []string{"message.txt"}},
		{name: "nested tree", files: map[string][]byte{"a/one.txt": []byte("one"), "a/b/two.txt": []byte("two")}, paths: []string{"a/one.txt", "a/b/two.txt"}},
	} {
		t.Run(test.name, func(t *testing.T) {
			fsys, err := New(test.files)
			if err != nil {
				t.Fatal(err)
			}
			if err := fstest.TestFS(fsys, test.paths...); err != nil {
				t.Fatal(err)
			}
		})
	}
}

func TestNewOwnsConstructionAndReadBytes(t *testing.T) {
	input := []byte("value")
	fsys, err := New(map[string][]byte{"value.txt": input})
	if err != nil {
		t.Fatal(err)
	}
	input[0] = 'x'
	first, err := fsys.ReadFile("value.txt")
	if err != nil {
		t.Fatal(err)
	}
	first[0] = 'y'
	second, err := fsys.ReadFile("value.txt")
	if err != nil {
		t.Fatal(err)
	}
	if string(second) != "value" {
		t.Fatalf("ReadFile() = %q, want owned bytes", second)
	}
}

func TestFilesystemStandardBehavior(t *testing.T) {
	fsys, err := New(map[string][]byte{
		"z.txt":       []byte("z"),
		"nested/a.md": []byte("abc"),
		"nested/b.md": []byte("b"),
	})
	if err != nil {
		t.Fatal(err)
	}
	entries, err := fsys.ReadDir("nested")
	if err != nil {
		t.Fatal(err)
	}
	if len(entries) != 2 || entries[0].Name() != "a.md" || entries[1].Name() != "b.md" {
		t.Fatalf("ReadDir(nested) = %#v, want sorted entries", entries)
	}
	info, err := entries[0].Info()
	if err != nil || info.Mode().Perm() != 0o444 || info.Size() != 3 {
		t.Fatalf("file info = %#v, %v", info, err)
	}
	root, err := fsys.Open(".")
	if err != nil {
		t.Fatal(err)
	}
	rootInfo, err := root.Stat()
	if err != nil || !rootInfo.IsDir() || rootInfo.Mode() != fs.ModeDir|0o555 {
		t.Fatalf("root info = %#v, %v", rootInfo, err)
	}
	directory := root.(fs.ReadDirFile)
	first, err := directory.ReadDir(1)
	if err != nil || len(first) != 1 || first[0].Name() != "nested" {
		t.Fatalf("first directory page = %#v, %v", first, err)
	}
	second, err := directory.ReadDir(1)
	if err != nil || len(second) != 1 || second[0].Name() != "z.txt" {
		t.Fatalf("second directory page = %#v, %v", second, err)
	}
	if _, err := directory.ReadDir(1); !errors.Is(err, io.EOF) {
		t.Fatalf("final directory page error = %v, want EOF", err)
	}
	secondRoot, err := fsys.Open(".")
	if err != nil {
		t.Fatal(err)
	}
	secondDirectory := secondRoot.(fs.ReadDirFile)
	firstAgain, err := secondDirectory.ReadDir(1)
	if err != nil || len(firstAgain) != 1 || firstAgain[0].Name() != "nested" {
		t.Fatalf("independent directory handle page = %#v, %v", firstAgain, err)
	}

	left, err := fsys.Open("nested/a.md")
	if err != nil {
		t.Fatal(err)
	}
	right, err := fsys.Open("nested/a.md")
	if err != nil {
		t.Fatal(err)
	}
	leftByte := make([]byte, 1)
	rightByte := make([]byte, 1)
	if _, err := left.Read(leftByte); err != nil {
		t.Fatal(err)
	}
	if _, err := right.Read(rightByte); err != nil {
		t.Fatal(err)
	}
	if string(leftByte) != "a" || string(rightByte) != "a" {
		t.Fatalf("independent file handles read %q and %q", leftByte, rightByte)
	}
	for _, test := range []struct {
		name string
		op   func() error
		want error
	}{
		{name: "invalid open path", op: func() error { _, err := fsys.Open("../value"); return err }, want: fs.ErrInvalid},
		{name: "missing file", op: func() error { _, err := fsys.Open("missing"); return err }, want: fs.ErrNotExist},
		{name: "read directory", op: func() error { _, err := fsys.ReadFile("nested"); return err }, want: errIsDirectory},
		{name: "read directory from file", op: func() error { _, err := fsys.ReadDir("z.txt"); return err }, want: errNotDirectory},
	} {
		t.Run(test.name, func(t *testing.T) {
			if err := test.op(); !errors.Is(err, test.want) {
				t.Fatalf("operation error = %v, want %v", err, test.want)
			}
		})
	}
}

func TestNewRejectsInvalidAndConflictingPaths(t *testing.T) {
	for _, files := range []map[string][]byte{
		{".": []byte("root")},
		{"": []byte("empty")},
		{"../outside": []byte("outside")},
		{"nested//file": []byte("invalid")},
		{"file": []byte("file"), "file/nested": []byte("nested")},
	} {
		if _, err := New(files); err == nil {
			t.Fatalf("New(%#v) error = nil, want rejection", files)
		}
	}
}
