/*
   Copyright The containerd Authors.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package manager

import (
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sync/atomic"
	"testing"
	"time"

	"github.com/containerd/errdefs"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/containerd/containerd/v2/core/metadata"
	"github.com/containerd/containerd/v2/core/mount"
	"github.com/containerd/containerd/v2/pkg/gc"
	"github.com/containerd/containerd/v2/pkg/namespaces"
	"github.com/containerd/containerd/v2/pkg/testutil"

	bolt "go.etcd.io/bbolt"
	bolterr "go.etcd.io/bbolt/errors"
)

func TestManager(t *testing.T) {
	testutil.RequiresRoot(t)
	td := t.TempDir()
	ctx := namespaces.WithNamespace(context.Background(), "test")

	sourcedir := filepath.Join(td, "source")
	if err := os.Mkdir(sourcedir, 0700); err != nil {
		t.Fatal(err)
	}
	mounts := []mount.Mount{
		{
			Type:    "bind",
			Source:  sourcedir,
			Options: []string{"rbind", "ro"},
		},
	}

	// newManager creates a fresh bolt DB and mount manager for each subtest so
	// that closing the manager (which now closes the DB) does not affect sibling
	// subtests.  It returns the manager and the target directory it was given.
	newManager := func(t *testing.T, opts ...Opt) (mount.Manager, string) {
		t.Helper()
		subtd := t.TempDir()
		db, err := bolt.Open(filepath.Join(subtd, "mounts.db"), 0600, nil)
		require.NoError(t, err)
		targetdir := filepath.Join(subtd, "m")
		m, err := NewManager(db, targetdir, opts...)
		require.NoError(t, err)
		t.Cleanup(func() { assert.NoError(t, m.(io.Closer).Close()) })
		return m, targetdir
	}

	t.Run("ActivateNoMounts", func(t *testing.T) {
		m, _ := newManager(t)
		_, err := m.Activate(ctx, "id1", []mount.Mount{})
		assert.ErrorIs(t, err, errdefs.ErrNotImplemented)
	})

	t.Run("SystemOnly", func(t *testing.T) {
		m, _ := newManager(t)
		_, err := m.Activate(ctx, "id1", mounts)
		assert.ErrorIs(t, err, errdefs.ErrNotImplemented)
	})

	t.Run("SystemOverride", func(t *testing.T) {
		m, targetdir := newManager(t, WithMountHandler("bind", &noopHandler{mounts: &atomic.Int32{}}))
		ainfo, err := m.Activate(ctx, "id1", mounts)
		require.NoError(t, err)
		defer assert.NoError(t, m.Deactivate(ctx, "id1"))

		assert.Equal(t, len(ainfo.Active), 1)
		assert.Equal(t, len(ainfo.System), 0)
		assert.Equal(t, ainfo.Active[0].Source, sourcedir)
		assert.Equal(t, ainfo.Active[0].Type, "bind")
		assert.Equal(t, ainfo.Active[0].MountPoint, filepath.Join(targetdir, "1", "1"))
	})

	// try mounting
	// Test mount

}

type noopHandler struct {
	mounts *atomic.Int32
}

func (h *noopHandler) Mount(ctx context.Context, m mount.Mount, mp string, _ []mount.ActiveMount) (mount.ActiveMount, error) {
	now := time.Now()
	h.mounts.Add(1)
	return mount.ActiveMount{
		Mount:      m,
		MountedAt:  &now,
		MountPoint: mp,
	}, nil
}

func (h *noopHandler) Unmount(context.Context, string) error {
	h.mounts.Add(-1)
	return nil
}

type errOnceHandler struct {
	mounts  *atomic.Int32
	mounted map[string]struct{}
}

func (h *errOnceHandler) Mount(_ context.Context, m mount.Mount, mp string, _ []mount.ActiveMount) (mount.ActiveMount, error) {
	h.mounted[mp] = struct{}{}
	h.mounts.Add(1)
	now := time.Now()
	return mount.ActiveMount{
		Mount:      m,
		MountedAt:  &now,
		MountPoint: mp,
		MountData:  nil,
	}, nil
}

func (h *errOnceHandler) Unmount(_ context.Context, mp string) error {
	if _, ok := h.mounted[mp]; ok {
		delete(h.mounted, mp)
		return fmt.Errorf("first unmount always fails")
	}
	h.mounts.Add(-1)
	return nil
}

// TestGC tests the garbage collecion features of the mount manager,
// ensuring that mounts are properly cleaned up when no longer needed.
func TestGC(t *testing.T) {
	type gcrun struct {
		a      []mount.Mount
		o      []mount.ActivateOpt
		d      []string
		all    []string
		active []string
		brefs  map[string][]string
		remove []string
		gcErr  bool
	}

	for _, tc := range []struct {
		name   string
		gcruns []gcrun
	}{
		{
			name: "Simple",
			gcruns: []gcrun{
				{
					a: []mount.Mount{
						{
							Type: "noop",
						},
					},
					all:    []string{"0-0"},
					remove: []string{},
				},
				{
					all:    []string{"0-0"},
					remove: []string{"0-0"},
				},
				{},
			},
		},
		{
			name: "UnmountError",
			gcruns: []gcrun{
				{
					a: []mount.Mount{
						{
							Type: "error",
						},
					},
					all:    []string{"0-0"},
					remove: []string{},
				},
				{
					all:    []string{"0-0"},
					remove: []string{"0-0"},
					gcErr:  true, // Expect an error on garbage collection due to unmount error
				},
				{}, // Run again without error to bring mount count back to zero
			},
		},
		{
			name: "ActiveBrefs",
			gcruns: []gcrun{
				{
					a: []mount.Mount{
						{
							Type: "noop",
						},
						{
							Type: "noop",
						},
					},
					o: []mount.ActivateOpt{
						mount.WithLabels(map[string]string{"containerd.io/gc.bref.container": "container1"}),
					},
					all: []string{"0-0", "0-1"},
					brefs: map[string][]string{
						"container1": {"0-0", "0-1"},
					},
					remove: []string{"0-1"},
				},
				{
					all:    []string{"0-0"},
					remove: []string{"0-0"},
					brefs: map[string][]string{
						"container1": {"0-0"},
					},
				},
				{
					brefs: map[string][]string{
						"container1": nil,
					},
				},
			},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			td := t.TempDir()
			metadb := filepath.Join(td, "mounts.db")
			targetdir := filepath.Join(td, "m")
			db, err := bolt.Open(metadb, 0600, nil)
			if err != nil {
				t.Fatal(err)
			}
			ctx := namespaces.WithNamespace(context.Background(), "test")

			sourcedir := filepath.Join(td, "source")
			if err := os.Mkdir(sourcedir, 0700); err != nil {
				t.Fatal(err)
			}
			mountC := new(atomic.Int32)
			m, err := NewManager(db, targetdir, WithMountHandler("noop", &noopHandler{mounts: mountC}), WithMountHandler("error", &errOnceHandler{mounts: mountC, mounted: make(map[string]struct{})}))
			require.NoError(t, err)
			t.Cleanup(func() {
				assert.NoError(t, m.(io.Closer).Close())
			})

			for i, run := range tc.gcruns {
				for j, mnt := range run.a {
					id := fmt.Sprintf("%d-%d", i, j)
					m.Activate(ctx, id, []mount.Mount{mnt}, run.o...)
				}

				for _, id := range run.d {
					if err := m.Deactivate(ctx, id); err != nil {
						t.Fatalf("deactivate %s: %v", id, err)
					}
				}

				cc, err := m.(interface {
					StartCollection(context.Context) (metadata.CollectionContext, error)
				}).StartCollection(ctx)
				require.NoError(t, err)

				var all []string

				checkGCActive(t, i, cc, run.active, run.brefs)

				cc.All(func(n gc.Node) {
					all = append(all, n.Key)
				})

				require.Equal(t, run.all, all, "run %d: all does not match", i)

				for _, id := range run.remove {
					cc.Remove(gc.Node{
						Type:      metadata.ResourceMount,
						Namespace: "test",
						Key:       id,
					})
				}

				err = cc.Finish()
				if run.gcErr && err == nil {
					t.Fatalf("expected error on run %d", i)
				} else if !run.gcErr && err != nil {
					t.Fatalf("unexpected error on run %d: %v", i, err)
				}

				// Interface functions not covered, cover in another test?
				// Active(namespace string, fn func(gc.Node))
				// Leased(namespace, lease string, fn func(gc.Node))
				// Cancel() error
			}
			if mountC.Load() != 0 {
				t.Fatalf("remaining mounts: %d", mountC.Load())
			}
		})
	}
}

func checkGCActive(t *testing.T, i int, cc metadata.CollectionContext, active []string, brefs map[string][]string) {
	t.Helper()
	ccb := cc.(interface {
		ActiveWithBackRefs(string, func(gc.Node), func(gc.Node, gc.Node))
	})

	var (
		activeKeys  []string
		activeBrefs = map[string][]string{}
	)
	ccb.ActiveWithBackRefs("test", func(n gc.Node) {
		activeKeys = append(activeKeys, n.Key)
	}, func(n, ref gc.Node) {
		activeBrefs[n.Key] = append(activeBrefs[n.Key], ref.Key)
	})

	require.Equal(t, active, activeKeys, "run %d: active does not match", i)

	for k := range brefs {
		require.Equal(t, brefs[k], activeBrefs[k], "run %d: brefs for %q does not match", i, k)
	}
}

func TestActivateAlreadyExists(t *testing.T) {
	td := t.TempDir()
	metadb := filepath.Join(td, "mounts.db")
	targetdir := filepath.Join(td, "m")
	db, err := bolt.Open(metadb, 0600, nil)
	require.NoError(t, err)
	ctx := namespaces.WithNamespace(context.Background(), "test")

	mountC := new(atomic.Int32)
	m, err := NewManager(db, targetdir, WithMountHandler("noop", &noopHandler{mounts: mountC}))
	require.NoError(t, err)
	t.Cleanup(func() { assert.NoError(t, m.(io.Closer).Close()) })

	mounts := []mount.Mount{{Type: "noop"}}

	// First activation should succeed
	_, err = m.Activate(ctx, "task1", mounts)
	require.NoError(t, err)

	// Second activation with same name should return ErrAlreadyExists
	_, err = m.Activate(ctx, "task1", mounts)
	assert.True(t, errdefs.IsAlreadyExists(err), "expected ErrAlreadyExists, got: %v", err)

	// Info should return valid info for the existing mount
	info, err := m.Info(ctx, "task1")
	require.NoError(t, err)
	assert.Equal(t, "task1", info.Name)
	assert.Equal(t, 1, len(info.Active))

	// Cleanup
	assert.NoError(t, m.Deactivate(ctx, "task1"))
}

func TestActivateStaleIncomplete(t *testing.T) {
	td := t.TempDir()
	metadb := filepath.Join(td, "mounts.db")
	targetdir := filepath.Join(td, "m")
	db, err := bolt.Open(metadb, 0600, nil)
	require.NoError(t, err)
	ctx := namespaces.WithNamespace(context.Background(), "test")

	// Simulate a stale incomplete activation by directly writing a bucket
	// without the "active" sub-bucket (as if the process crashed mid-activation).
	// Use mount ID 42 so we can verify the stale target directory gets cleaned up.
	var staleMID uint64 = 42
	err = db.Update(func(tx *bolt.Tx) error {
		v1bkt, err := tx.CreateBucketIfNotExists([]byte("v1"))
		if err != nil {
			return err
		}
		nsbkt, err := v1bkt.CreateBucketIfNotExists([]byte("test"))
		if err != nil {
			return err
		}
		mbkt, err := nsbkt.CreateBucketIfNotExists(bucketKeyMounts)
		if err != nil {
			return err
		}
		// Create the mount bucket but don't add the "active" sub-bucket
		bkt, err := mbkt.CreateBucket([]byte("task1"))
		if err != nil {
			return err
		}
		idb, err := encodeID(staleMID)
		if err != nil {
			return err
		}
		return bkt.Put(bucketKeyID, idb)
	})
	require.NoError(t, err)

	// Create the stale target directory as if the process crashed after
	// mkdir but before the second transaction.
	staleTarget := filepath.Join(targetdir, fmt.Sprintf("%d", staleMID))
	require.NoError(t, os.MkdirAll(staleTarget, 0700))

	mountC := new(atomic.Int32)
	m, err := NewManager(db, targetdir, WithMountHandler("noop", &noopHandler{mounts: mountC}))
	require.NoError(t, err)
	t.Cleanup(func() { assert.NoError(t, m.(io.Closer).Close()) })

	mounts := []mount.Mount{{Type: "noop"}}

	// Activation should succeed by cleaning up the stale entry
	ainfo, err := m.Activate(ctx, "task1", mounts)
	require.NoError(t, err)
	assert.Equal(t, "task1", ainfo.Name)
	assert.Equal(t, 1, len(ainfo.Active))

	// The stale target directory should have been cleaned up
	_, err = os.Stat(staleTarget)
	assert.True(t, os.IsNotExist(err), "stale target directory should be removed, but still exists")

	// Cleanup
	assert.NoError(t, m.Deactivate(ctx, "task1"))
}

func TestInfo(t *testing.T) {
	td := t.TempDir()
	metadb := filepath.Join(td, "mounts.db")
	targetdir := filepath.Join(td, "m")
	db, err := bolt.Open(metadb, 0600, nil)
	require.NoError(t, err)
	ctx := namespaces.WithNamespace(context.Background(), "test")

	mountC := new(atomic.Int32)
	m, err := NewManager(db, targetdir, WithMountHandler("noop", &noopHandler{mounts: mountC}))
	require.NoError(t, err)
	t.Cleanup(func() { assert.NoError(t, m.(io.Closer).Close()) })

	// Info on non-existent mount should return ErrNotFound
	_, err = m.Info(ctx, "nonexistent")
	assert.True(t, errdefs.IsNotFound(err), "expected ErrNotFound, got: %v", err)

	// Activate a mount with labels
	labels := map[string]string{
		"containerd.io/gc.bref.container": "ctr1",
		"custom-label":                    "value1",
	}
	mounts := []mount.Mount{{Type: "noop"}}
	ainfo, err := m.Activate(ctx, "task1", mounts, mount.WithLabels(labels))
	require.NoError(t, err)
	defer m.Deactivate(ctx, "task1")

	// Info should return the correct activation info
	info, err := m.Info(ctx, "task1")
	require.NoError(t, err)
	assert.Equal(t, "task1", info.Name)
	assert.Equal(t, 1, len(info.Active))
	assert.Equal(t, "noop", info.Active[0].Type)
	assert.NotNil(t, info.Active[0].MountedAt)
	assert.Equal(t, labels, info.Labels)

	// Info active and system mounts should match those returned by Activate
	require.Equal(t, len(ainfo.Active), len(info.Active))
	for i := range ainfo.Active {
		assert.Equal(t, ainfo.Active[i].Type, info.Active[i].Type)
		assert.Equal(t, ainfo.Active[i].MountPoint, info.Active[i].MountPoint)
		assert.Equal(t, ainfo.Active[i].MountedAt.Unix(), info.Active[i].MountedAt.Unix())
	}
	// No system mounts when all mounts are handled
	assert.Empty(t, ainfo.System)
	assert.Empty(t, info.System)

	// Activate a second mount and verify Info returns correct data for each
	_, err = m.Activate(ctx, "task2", mounts)
	require.NoError(t, err)
	defer m.Deactivate(ctx, "task2")

	info2, err := m.Info(ctx, "task2")
	require.NoError(t, err)
	assert.Equal(t, "task2", info2.Name)
	assert.Equal(t, 1, len(info2.Active))
	// task2 has no custom labels
	assert.Empty(t, info2.Labels)

	// Original task1 info should be unchanged
	info1, err := m.Info(ctx, "task1")
	require.NoError(t, err)
	assert.Equal(t, "task1", info1.Name)
	assert.Equal(t, labels, info1.Labels)
}

func TestInfoSystemMounts(t *testing.T) {
	td := t.TempDir()
	metadb := filepath.Join(td, "mounts.db")
	targetdir := filepath.Join(td, "m")
	db, err := bolt.Open(metadb, 0600, nil)
	require.NoError(t, err)
	ctx := namespaces.WithNamespace(context.Background(), "test")

	mountC := new(atomic.Int32)
	// Only register a handler for "noop"; "bind" will pass through as a system mount
	m, err := NewManager(db, targetdir, WithMountHandler("noop", &noopHandler{mounts: mountC}))
	require.NoError(t, err)
	t.Cleanup(func() { assert.NoError(t, m.(io.Closer).Close()) })

	sourcedir := filepath.Join(td, "source")
	require.NoError(t, os.Mkdir(sourcedir, 0700))

	mounts := []mount.Mount{
		{Type: "noop"},
		{
			Type:    "bind",
			Source:  sourcedir,
			Options: []string{"rbind", "ro"},
		},
	}

	ainfo, err := m.Activate(ctx, "task1", mounts)
	require.NoError(t, err)
	defer m.Deactivate(ctx, "task1")

	// Activate should return one active mount and one system mount
	require.Equal(t, 1, len(ainfo.Active))
	require.Equal(t, 1, len(ainfo.System))
	assert.Equal(t, "bind", ainfo.System[0].Type)
	assert.Equal(t, sourcedir, ainfo.System[0].Source)
	assert.Equal(t, []string{"rbind", "ro"}, ainfo.System[0].Options)

	// Info should return the same active and system mounts
	info, err := m.Info(ctx, "task1")
	require.NoError(t, err)
	assert.Equal(t, "task1", info.Name)

	require.Equal(t, len(ainfo.Active), len(info.Active))
	for i := range ainfo.Active {
		assert.Equal(t, ainfo.Active[i].Type, info.Active[i].Type)
		assert.Equal(t, ainfo.Active[i].MountPoint, info.Active[i].MountPoint)
		assert.Equal(t, ainfo.Active[i].MountedAt.Unix(), info.Active[i].MountedAt.Unix())
	}

	require.Equal(t, len(ainfo.System), len(info.System))
	for i := range ainfo.System {
		assert.Equal(t, ainfo.System[i].Type, info.System[i].Type)
		assert.Equal(t, ainfo.System[i].Source, info.System[i].Source)
		assert.Equal(t, ainfo.System[i].Target, info.System[i].Target)
		assert.Equal(t, ainfo.System[i].Options, info.System[i].Options)
	}
}

func TestActivateConcurrentSameName(t *testing.T) {
	td := t.TempDir()
	metadb := filepath.Join(td, "mounts.db")
	targetdir := filepath.Join(td, "m")
	db, err := bolt.Open(metadb, 0600, nil)
	require.NoError(t, err)
	ctx := namespaces.WithNamespace(context.Background(), "test")

	mountC := new(atomic.Int32)
	m, err := NewManager(db, targetdir, WithMountHandler("noop", &noopHandler{mounts: mountC}))
	require.NoError(t, err)
	t.Cleanup(func() { assert.NoError(t, m.(io.Closer).Close()) })

	mounts := []mount.Mount{{Type: "noop"}}

	// Launch two concurrent activations with the same name.
	// The per-name lock should serialize them: first succeeds,
	// second gets ErrAlreadyExists (not a stale-recovery race).
	errs := make(chan error, 2)
	for i := 0; i < 2; i++ {
		go func() {
			_, err := m.Activate(ctx, "task1", mounts)
			errs <- err
		}()
	}

	err1 := <-errs
	err2 := <-errs

	// Exactly one should succeed and one should get ErrAlreadyExists
	if err1 == nil && errdefs.IsAlreadyExists(err2) {
		// ok
	} else if err2 == nil && errdefs.IsAlreadyExists(err1) {
		// ok
	} else {
		t.Fatalf("expected one nil and one ErrAlreadyExists, got: %v, %v", err1, err2)
	}

	assert.NoError(t, m.Deactivate(ctx, "task1"))
}

// TODO: Test deactivate
// TODO: Test Sync

func TestClose(t *testing.T) {
	td := t.TempDir()
	db, err := bolt.Open(filepath.Join(td, "mounts.db"), 0600, nil)
	require.NoError(t, err)

	m, err := NewManager(db, filepath.Join(td, "m"))
	require.NoError(t, err)

	require.NoError(t, m.(io.Closer).Close())

	// Verify the underlying bolt DB is closed: a new transaction should fail.
	_, err = db.Begin(false)
	assert.ErrorIs(t, err, bolterr.ErrDatabaseNotOpen)
}
