package mount

import (
	"context"
	"net"
	"path/filepath"
	"sync"
	"syscall"
	"testing"
	"time"

	"github.com/seaweedfs/go-fuse/v2/fuse"
	"github.com/seaweedfs/seaweedfs/weed/filer"
	"github.com/seaweedfs/seaweedfs/weed/mount/meta_cache"
	"github.com/seaweedfs/seaweedfs/weed/pb"
	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
	"github.com/seaweedfs/seaweedfs/weed/util"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

type createEntryTestServer struct {
	filer_pb.UnimplementedSeaweedFilerServer
	mu            sync.Mutex
	lastDirectory string
	lastName      string
	lastUID       uint32
	lastGID       uint32
	lastMode      uint32
}

type createEntrySnapshot struct {
	directory string
	name      string
	uid       uint32
	gid       uint32
	mode      uint32
}

func (s *createEntryTestServer) CreateEntry(ctx context.Context, req *filer_pb.CreateEntryRequest) (*filer_pb.CreateEntryResponse, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.lastDirectory = req.GetDirectory()
	if req.GetEntry() != nil {
		s.lastName = req.GetEntry().GetName()
		if req.GetEntry().GetAttributes() != nil {
			s.lastUID = req.GetEntry().GetAttributes().GetUid()
			s.lastGID = req.GetEntry().GetAttributes().GetGid()
			s.lastMode = req.GetEntry().GetAttributes().GetFileMode()
		}
	}
	return &filer_pb.CreateEntryResponse{}, nil
}

func (s *createEntryTestServer) UpdateEntry(ctx context.Context, req *filer_pb.UpdateEntryRequest) (*filer_pb.UpdateEntryResponse, error) {
	return &filer_pb.UpdateEntryResponse{}, nil
}

func (s *createEntryTestServer) snapshot() createEntrySnapshot {
	s.mu.Lock()
	defer s.mu.Unlock()
	return createEntrySnapshot{
		directory: s.lastDirectory,
		name:      s.lastName,
		uid:       s.lastUID,
		gid:       s.lastGID,
		mode:      s.lastMode,
	}
}

func newCreateTestWFS(t *testing.T) (*WFS, *createEntryTestServer) {
	t.Helper()

	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	t.Cleanup(func() {
		_ = listener.Close()
	})

	server := pb.NewGrpcServer()
	testServer := &createEntryTestServer{}
	filer_pb.RegisterSeaweedFilerServer(server, testServer)
	go server.Serve(listener)
	t.Cleanup(server.Stop)

	uidGidMapper, err := meta_cache.NewUidGidMapper("", "")
	if err != nil {
		t.Fatalf("create uid/gid mapper: %v", err)
	}

	root := util.FullPath("/")
	option := &Option{
		ChunkSizeLimit:     1024,
		ConcurrentReaders:  1,
		VolumeServerAccess: "filerProxy",
		FilerAddresses: []pb.ServerAddress{
			pb.NewServerAddressWithGrpcPort("127.0.0.1:1", listener.Addr().(*net.TCPAddr).Port),
		},
		GrpcDialOption:         grpc.WithTransportCredentials(insecure.NewCredentials()),
		FilerMountRootPath:     "/",
		MountUid:               99,
		MountGid:               100,
		MountMode:              0o777,
		MountMtime:             time.Now(),
		MountCtime:             time.Now(),
		UidGidMapper:           uidGidMapper,
		uniqueCacheDirForWrite: t.TempDir(),
	}

	wfs := &WFS{
		option:            option,
		signature:         1,
		inodeToPath:       NewInodeToPath(root, 0),
		fhMap:             NewFileHandleToInode(),
		fhLockTable:       util.NewLockTable[FileHandleId](),
		hardLinkLockTable: util.NewLockTable[string](),
	}
	wfs.metaCache = meta_cache.NewMetaCache(
		filepath.Join(t.TempDir(), "meta"),
		uidGidMapper,
		root,
		false,
		func(path util.FullPath) {
			wfs.inodeToPath.MarkChildrenCached(path)
		},
		func(path util.FullPath) bool {
			return wfs.inodeToPath.IsChildrenCached(path)
		},
		func(meta_cache.EntryInvalidation) {},
		nil,
	)
	wfs.inodeToPath.MarkChildrenCached(root)
	t.Cleanup(func() {
		wfs.metaCache.Shutdown()
	})

	return wfs, testServer
}

func TestCreateCreatesAndOpensFile(t *testing.T) {
	wfs, testServer := newCreateTestWFS(t)

	out := &fuse.CreateOut{}
	status := wfs.Create(make(chan struct{}), &fuse.CreateIn{
		InHeader: fuse.InHeader{
			NodeId: 1,
			Caller: fuse.Caller{
				Owner: fuse.Owner{
					Uid: 123,
					Gid: 456,
				},
			},
		},
		Flags: syscall.O_WRONLY | syscall.O_CREAT,
		Mode:  0o640,
	}, "hello.txt", out)
	if status != fuse.OK {
		t.Fatalf("Create status = %v, want OK", status)
	}
	if out.NodeId == 0 {
		t.Fatal("Create returned zero inode")
	}
	if out.Fh == 0 {
		t.Fatal("Create returned zero file handle")
	}
	if out.OpenFlags != 0 {
		t.Fatalf("Create returned OpenFlags = %#x, want 0", out.OpenFlags)
	}

	fileHandle := wfs.GetHandle(FileHandleId(out.Fh))
	if fileHandle == nil {
		t.Fatal("Create did not register an open file handle")
	}
	if got := fileHandle.FullPath(); got != "/hello.txt" {
		t.Fatalf("FullPath = %q, want %q", got, "/hello.txt")
	}

	// File creation is deferred to flush time. Trigger a synchronous flush
	// so the CreateEntry gRPC call is sent to the test server.
	if flushStatus := wfs.Flush(make(chan struct{}), &fuse.FlushIn{
		InHeader: fuse.InHeader{
			NodeId: out.NodeId,
			Caller: fuse.Caller{Owner: fuse.Owner{Uid: 123, Gid: 456}},
		},
		Fh: out.Fh,
	}); flushStatus != fuse.OK {
		t.Fatalf("Flush status = %v, want OK", flushStatus)
	}

	snapshot := testServer.snapshot()
	if snapshot.directory != "/" {
		t.Fatalf("CreateEntry directory = %q, want %q", snapshot.directory, "/")
	}
	if snapshot.name != "hello.txt" {
		t.Fatalf("CreateEntry name = %q, want %q", snapshot.name, "hello.txt")
	}
	if snapshot.uid != 123 || snapshot.gid != 456 {
		t.Fatalf("CreateEntry uid/gid = %d/%d, want 123/456", snapshot.uid, snapshot.gid)
	}
	if snapshot.mode != 0o640 {
		t.Fatalf("CreateEntry mode = %o, want %o", snapshot.mode, 0o640)
	}
}

func TestReleaseFlushesDirtyCreateIfFlushWasSkipped(t *testing.T) {
	wfs, testServer := newCreateTestWFS(t)

	out := &fuse.CreateOut{}
	status := wfs.Create(make(chan struct{}), &fuse.CreateIn{
		InHeader: fuse.InHeader{
			NodeId: 1,
			Caller: fuse.Caller{
				Owner: fuse.Owner{
					Uid: 123,
					Gid: 456,
				},
			},
		},
		Flags: syscall.O_WRONLY | syscall.O_CREAT,
		Mode:  0o640,
	}, "release_flush.txt", out)
	if status != fuse.OK {
		t.Fatalf("Create status = %v, want OK", status)
	}

	wfs.Release(make(chan struct{}), &fuse.ReleaseIn{
		InHeader: fuse.InHeader{
			NodeId: out.NodeId,
			Caller: fuse.Caller{Owner: fuse.Owner{Uid: 123, Gid: 456}},
		},
		Fh: out.Fh,
	})

	snapshot := testServer.snapshot()
	if snapshot.directory != "/" {
		t.Fatalf("CreateEntry directory = %q, want %q", snapshot.directory, "/")
	}
	if snapshot.name != "release_flush.txt" {
		t.Fatalf("CreateEntry name = %q, want %q", snapshot.name, "release_flush.txt")
	}
	if snapshot.uid != 123 || snapshot.gid != 456 {
		t.Fatalf("CreateEntry uid/gid = %d/%d, want 123/456", snapshot.uid, snapshot.gid)
	}
	if snapshot.mode != 0o640 {
		t.Fatalf("CreateEntry mode = %o, want %o", snapshot.mode, 0o640)
	}
	if fh := wfs.GetHandle(FileHandleId(out.Fh)); fh != nil {
		t.Fatal("Release should remove the file handle after fallback flush")
	}
}

func TestTruncateEntryClearsDirtyPagesForOpenHandle(t *testing.T) {
	wfs, _ := newCreateTestWFS(t)

	fullPath := util.FullPath("/truncate.txt")
	inode := wfs.inodeToPath.Lookup(fullPath, 1, false, false, 0, true)
	entry := &filer_pb.Entry{
		Name: "truncate.txt",
		Attributes: &filer_pb.FuseAttributes{
			FileMode: 0o644,
			FileSize: 5,
			Inode:    inode,
			Crtime:   1,
			Mtime:    1,
		},
	}

	fh, _ := wfs.fhMap.AcquireFileHandle(wfs, inode, entry, 0, 0)
	fh.RememberPath(fullPath)

	if err := fh.dirtyPages.AddPage(0, []byte("hello"), true, time.Now().UnixNano()); err != nil {
		t.Fatalf("AddPage: %v", err)
	}
	oldDirtyPages := fh.dirtyPages

	truncatedEntry := &filer_pb.Entry{
		Name: "truncate.txt",
		Attributes: &filer_pb.FuseAttributes{
			FileMode: 0o644,
			FileSize: 5,
			Inode:    inode,
			Crtime:   1,
			Mtime:    1,
		},
	}

	if status := wfs.truncateEntry(fullPath, truncatedEntry); status != fuse.OK {
		t.Fatalf("truncateEntry status = %v, want OK", status)
	}
	if fh.dirtyPages == oldDirtyPages {
		t.Fatal("truncateEntry should replace the dirtyPages writer for an open handle")
	}
	if got := fh.GetEntry().GetEntry().GetAttributes().GetFileSize(); got != 0 {
		t.Fatalf("file handle size = %d, want 0", got)
	}
	buf := make([]byte, 5)
	if maxStop := fh.dirtyPages.ReadDirtyDataAt(buf, 0, time.Now().UnixNano()); maxStop != 0 {
		t.Fatalf("dirty pages maxStop = %d, want 0 after truncate", maxStop)
	}
}

func TestAccessChecksPermissions(t *testing.T) {
	wfs := newCopyRangeTestWFS()
	oldLookupSupplementaryGroupIDs := lookupSupplementaryGroupIDs
	lookupSupplementaryGroupIDs = func(uint32) ([]string, error) {
		return nil, nil
	}
	clearSupplementaryGroupCache()
	t.Cleanup(func() {
		lookupSupplementaryGroupIDs = oldLookupSupplementaryGroupIDs
		clearSupplementaryGroupCache()
	})

	fullPath := util.FullPath("/visible.txt")
	inode := wfs.inodeToPath.Lookup(fullPath, 1, false, false, 0, true)
	handle, _ := wfs.fhMap.AcquireFileHandle(wfs, inode, &filer_pb.Entry{
		Name: "visible.txt",
		Attributes: &filer_pb.FuseAttributes{
			FileMode: 0o640,
			Uid:      123,
			Gid:      456,
			Inode:    inode,
		},
	}, 0, 0)
	handle.RememberPath(fullPath)

	if status := wfs.Access(make(chan struct{}), &fuse.AccessIn{
		InHeader: fuse.InHeader{
			NodeId: inode,
			Caller: fuse.Caller{
				Owner: fuse.Owner{
					Uid: 123,
					Gid: 999,
				},
			},
		},
		Mask: fuse.R_OK | fuse.W_OK,
	}); status != fuse.OK {
		t.Fatalf("owner Access status = %v, want OK", status)
	}

	if status := wfs.Access(make(chan struct{}), &fuse.AccessIn{
		InHeader: fuse.InHeader{
			NodeId: inode,
			Caller: fuse.Caller{
				Owner: fuse.Owner{
					Uid: 999,
					Gid: 999,
				},
			},
		},
		Mask: fuse.W_OK,
	}); status != fuse.EACCES {
		t.Fatalf("other-user Access status = %v, want EACCES", status)
	}

	if got := hasAccess(123, 999, 123, 456, 0o400, fuse.R_OK|fuse.W_OK); got {
		t.Fatal("owner should not get write access from a read-only owner mode")
	}

	if got := hasAccess(999, 456, 123, 456, 0o040, fuse.R_OK|fuse.W_OK); got {
		t.Fatal("group member should not get write access from a read-only group mode")
	}

	if got := hasAccess(999, 999, 123, 456, 0o004, fuse.R_OK|fuse.W_OK); got {
		t.Fatal("other users should not get write access from a read-only other mode")
	}

	if got := hasAccess(0, 0, 123, 456, 0o644, fuse.X_OK); got {
		t.Fatal("root should not get execute access when no execute bit is set")
	}

	if got := hasAccess(0, 0, 123, 456, 0o755, fuse.R_OK|fuse.X_OK); !got {
		t.Fatal("root should get execute access when at least one execute bit is set")
	}
}

func TestHasAccessUsesSupplementaryGroups(t *testing.T) {
	oldLookupSupplementaryGroupIDs := lookupSupplementaryGroupIDs
	lookupSupplementaryGroupIDs = func(uint32) ([]string, error) {
		return []string{"456"}, nil
	}
	clearSupplementaryGroupCache()
	t.Cleanup(func() {
		lookupSupplementaryGroupIDs = oldLookupSupplementaryGroupIDs
		clearSupplementaryGroupCache()
	})

	if got := hasAccess(999, 999, 123, 456, 0o060, fuse.R_OK|fuse.W_OK); !got {
		t.Fatal("supplementary group membership should grant matching group permissions")
	}
}

func TestSupplementaryGroupCaching(t *testing.T) {
	callCount := 0
	oldLookupSupplementaryGroupIDs := lookupSupplementaryGroupIDs
	lookupSupplementaryGroupIDs = func(uid uint32) ([]string, error) {
		callCount++
		return []string{"456"}, nil
	}
	clearSupplementaryGroupCache()
	t.Cleanup(func() {
		lookupSupplementaryGroupIDs = oldLookupSupplementaryGroupIDs
		clearSupplementaryGroupCache()
	})

	cachedLookupSupplementaryGroupIDs(999)
	cachedLookupSupplementaryGroupIDs(999)
	cachedLookupSupplementaryGroupIDs(999)

	if callCount != 1 {
		t.Fatalf("lookupSupplementaryGroupIDs called %d times, expected 1 (cache should prevent repeated calls)", callCount)
	}

	cachedLookupSupplementaryGroupIDs(1000)
	if callCount != 2 {
		t.Fatalf("lookupSupplementaryGroupIDs called %d times after different UID, expected 2", callCount)
	}
}

func TestSupplementaryGroupCacheExpiry(t *testing.T) {
	callCount := 0
	oldLookupSupplementaryGroupIDs := lookupSupplementaryGroupIDs
	oldTTL := supplementaryGroupCacheTTL
	supplementaryGroupCacheTTL = 0
	lookupSupplementaryGroupIDs = func(uid uint32) ([]string, error) {
		callCount++
		return []string{"456"}, nil
	}
	clearSupplementaryGroupCache()
	t.Cleanup(func() {
		lookupSupplementaryGroupIDs = oldLookupSupplementaryGroupIDs
		supplementaryGroupCacheTTL = oldTTL
		clearSupplementaryGroupCache()
	})

	cachedLookupSupplementaryGroupIDs(999)
	if callCount != 1 {
		t.Fatalf("Expected 1 lookup on first call, got %d", callCount)
	}

	cachedLookupSupplementaryGroupIDs(999)
	if callCount != 2 {
		t.Fatalf("Expected 2 lookups after TTL expiry, got %d", callCount)
	}
}

func TestCreateExistingFileIgnoresQuotaPreflight(t *testing.T) {
	wfs, _ := newCreateTestWFS(t)
	wfs.option.Quota = 1
	wfs.IsOverQuota = true

	entry := &filer_pb.Entry{
		Name: "existing.txt",
		Attributes: &filer_pb.FuseAttributes{
			FileMode: 0o644,
			FileSize: 7,
			Inode:    101,
			Crtime:   1,
			Mtime:    1,
			Uid:      123,
			Gid:      456,
		},
	}
	if err := wfs.metaCache.InsertEntry(context.Background(), filer.FromPbEntry("/", entry), 0); err != nil {
		t.Fatalf("InsertEntry: %v", err)
	}
	wfs.inodeToPath.Lookup(util.FullPath("/existing.txt"), entry.Attributes.Crtime, false, false, entry.Attributes.Inode, true)

	out := &fuse.CreateOut{}
	status := wfs.Create(make(chan struct{}), &fuse.CreateIn{
		InHeader: fuse.InHeader{
			NodeId: 1,
			Caller: fuse.Caller{
				Owner: fuse.Owner{
					Uid: 123,
					Gid: 456,
				},
			},
		},
		Flags: syscall.O_WRONLY | syscall.O_CREAT | syscall.O_EXCL,
		Mode:  0o644,
	}, "existing.txt", out)
	if status != fuse.Status(syscall.EEXIST) {
		t.Fatalf("Create status = %v, want EEXIST", status)
	}
}

// With default_permissions the kernel enforces unix bits before it calls
// Open, so AcquireHandle must skip its own check (and the group lookup behind
// it). Without it, AcquireHandle stays the enforcer.
func TestAcquireHandleHonorsDefaultPermissions(t *testing.T) {
	for _, tc := range []struct {
		name               string
		defaultPermissions bool
		want               fuse.Status
	}{
		{"kernel enforces", true, fuse.OK},
		{"mount enforces", false, fuse.EACCES},
	} {
		t.Run(tc.name, func(t *testing.T) {
			wfs, _ := newCreateTestWFS(t)
			wfs.option.DefaultPermissions = tc.defaultPermissions

			oldLookup := lookupSupplementaryGroupIDs
			lookupSupplementaryGroupIDs = func(uint32) ([]string, error) { return nil, nil }
			clearSupplementaryGroupCache()
			t.Cleanup(func() {
				lookupSupplementaryGroupIDs = oldLookup
				clearSupplementaryGroupCache()
			})

			entry := &filer_pb.Entry{
				Name: "secret.txt",
				Attributes: &filer_pb.FuseAttributes{
					FileMode: 0o600, // owner-only: an "other" uid has no read
					Inode:    202,
					Crtime:   1,
					Mtime:    1,
					Uid:      123,
					Gid:      456,
				},
			}
			if err := wfs.metaCache.InsertEntry(context.Background(), filer.FromPbEntry("/", entry), 0); err != nil {
				t.Fatalf("InsertEntry: %v", err)
			}
			inode := wfs.inodeToPath.Lookup(util.FullPath("/secret.txt"), entry.Attributes.Crtime, false, false, entry.Attributes.Inode, true)

			fh, status := wfs.AcquireHandle(inode, syscall.O_RDONLY, 999, 999)
			if status != tc.want {
				t.Fatalf("AcquireHandle status = %v, want %v", status, tc.want)
			}
			if status == fuse.OK {
				if fh == nil {
					t.Fatal("AcquireHandle returned nil handle on OK")
				}
				wfs.ReleaseHandle(fh.fh)
			}
		})
	}
}

// default_permissions skips only the mode-bit check, not parent-existence
// validation: the create RPC sets SkipCheckParentDirectory, so the mount's
// own parent lookup is the only thing guarding against an orphaned entry.
func TestCreateRegularFileValidatesParentUnderDefaultPermissions(t *testing.T) {
	wfs, _ := newCreateTestWFS(t)
	wfs.option.DefaultPermissions = true

	if _, _, code := wfs.createRegularFile(util.FullPath("/ghost"), "f.txt", 0o644, 99, 100, 0, false, false); code == fuse.OK {
		t.Fatal("createRegularFile returned OK for a missing parent directory")
	}
}
