// Copyright Istio 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 nodeagent

import (
	"context"
	"embed"
	"errors"
	"io/fs"
	"strings"
	"sync/atomic"
	"syscall"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/types"

	"istio.io/istio/cni/pkg/config"
	"istio.io/istio/cni/pkg/iptables"
)

//go:embed testdata/cgroupns
var fakeProc embed.FS

type fakeZtunnel struct {
	deletedPods atomic.Int32
	addedPods   atomic.Int32
	addError    error
	delError    error
}

func (f *fakeZtunnel) Run(ctx context.Context) {
}

func (f *fakeZtunnel) PodDeleted(ctx context.Context, uid string) error {
	f.deletedPods.Add(1)
	return f.delError
}

func (f *fakeZtunnel) PodAdded(ctx context.Context, pod *corev1.Pod, netns Netns) error {
	f.addedPods.Add(1)
	return f.addError
}

func (f *fakeZtunnel) Close() error {
	return nil
}

// fakeNs is a mock struct for testing
type fakeNs struct {
	closed    *atomic.Bool
	fd        uintptr
	inode     uint64
	starttime uint64
}

func newFakeNs(fd uintptr) *fakeNs {
	// the fake inode is the fd! magic.
	return &fakeNs{closed: &atomic.Bool{}, fd: fd, inode: uint64(fd)}
}

func newFakeNsInode(fd uintptr, inode uint64) *fakeNs {
	return &fakeNs{closed: &atomic.Bool{}, fd: fd, inode: inode}
}

// Fd returns the file descriptor
func (f *fakeNs) Fd() uintptr {
	return f.fd
}

func (f *fakeNs) Inode() uint64 {
	return f.inode
}

func (f *fakeNs) OwnerProcStarttime() uint64 {
	return f.starttime
}

// Close simulates closing the file descriptor and returns nil for no error
func (f *fakeNs) Close() error {
	f.closed.Store(true)
	return nil
}

func fakeFs(uniqueInos bool) *fakeFsWithFakeFds {
	subFs, err := fs.Sub(fakeProc, "testdata")
	if err != nil {
		panic(err)
	}
	subFs, err = fs.Sub(subFs, "cgroupns")
	if err != nil {
		panic(err)
	}
	return &fakeFsWithFakeFds{ReadDirFS: subFs.(fs.ReadDirFS), uniqueInos: uniqueInos}
}

// fakeFsWithNetnsInos is fakeFs(true) with fixed inodes for the given paths
// (e.g. "3/ns/net"), letting tests model processes that share a netns.
func fakeFsWithNetnsInos(inos map[string]int) *fakeFsWithFakeFds {
	ffs := fakeFs(true)
	ffs.inos = inos
	return ffs
}

type fakeFsWithFakeFds struct {
	fs.ReadDirFS
	inoCounter int
	uniqueInos bool
	inos       map[string]int
	opened     []*fakeFileFakeFds
}

// Open opens the named file.
// When Open returns an error, it should be of type *PathError
// with the Op field set to "open", the Path field set to name,
// and the Err field describing the problem.
//
// Open should reject attempts to open names that do not satisfy
// ValidPath(name), returning a *PathError with Err set to
// ErrInvalid or ErrNotExist.
func (ffs *fakeFsWithFakeFds) Open(name string) (fs.File, error) {
	f, err := ffs.ReadDirFS.Open(name)
	if err != nil {
		return nil, err
	}
	if ffs.uniqueInos {
		ffs.inoCounter++
	}
	ino := ffs.inoCounter
	if override, ok := ffs.inos[name]; ok {
		ino = override
	}
	wrapped := wrapFile(name, f, ino)
	ffs.opened = append(ffs.opened, wrapped)
	return wrapped, nil
}

// openNetnsFiles returns how many netns files handed out so far have not been closed.
func (ffs *fakeFsWithFakeFds) openNetnsFiles() int {
	open := 0
	for _, f := range ffs.opened {
		if strings.HasSuffix(f.name, "ns/net") && !f.closed {
			open++
		}
	}
	return open
}

func wrapFile(name string, f fs.File, ino int) *fakeFileFakeFds {
	return &fakeFileFakeFds{File: f, name: name, fd: 0, ino: ino}
}

type fakeFileFakeFds struct {
	fs.File
	name   string
	fd     uintptr
	ino    int
	closed bool
}

func (f *fakeFileFakeFds) Close() error {
	f.closed = true
	return f.File.Close()
}

func (f *fakeFileFakeFds) Fd() uintptr {
	return f.fd
}

func (f *fakeFileFakeFds) Stat() (fs.FileInfo, error) {
	fi, err := f.File.Stat()
	if err != nil {
		return nil, err
	}
	return &fakeFileFakeFI{FileInfo: fi, ino: f.ino}, nil
}

type fakeFileFakeFI struct {
	fs.FileInfo
	ino int
}

func (f *fakeFileFakeFI) Sys() any {
	return &syscall.Stat_t{Ino: uint64(f.ino), Dev: 3}
}

type fakeIptablesDeps struct {
	AddRouteErr           error
	AddInpodMarkIPRuleCnt atomic.Int32
	DelInpodMarkIPRuleCnt atomic.Int32
	AddLoopbackRoutesCnt  atomic.Int32
	DelLoopbackRoutesCnt  atomic.Int32
}

var _ iptables.NetlinkDependencies = &fakeIptablesDeps{}

func (r *fakeIptablesDeps) AddInpodMarkIPRule(cfg *config.AmbientConfig) error {
	r.AddInpodMarkIPRuleCnt.Add(1)
	return nil
}

func (r *fakeIptablesDeps) DelInpodMarkIPRule(cfg *config.AmbientConfig) error {
	r.DelInpodMarkIPRuleCnt.Add(1)
	return nil
}

func (r *fakeIptablesDeps) AddLoopbackRoutes(cfg *config.AmbientConfig) error {
	r.AddLoopbackRoutesCnt.Add(1)
	return r.AddRouteErr
}

func (r *fakeIptablesDeps) DelLoopbackRoutes(cfg *config.AmbientConfig) error {
	r.DelLoopbackRoutesCnt.Add(1)
	return nil
}

type NoOpPodNetnsProcFinder struct{}

func (p *NoOpPodNetnsProcFinder) FindNetnsForPods(pods map[types.UID]*corev1.Pod) (PodToNetns, error) {
	return make(PodToNetns), errors.New("NoOpPodNetnsProcFinder always returns an error")
}
