package client_test

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

	cli "github.com/linuxdeepin/go-rofs-err-watcher/pkg/client"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/sys/unix"
)

const (
	testFilePath          = "/usr/share/man/man1/dash.1.gz"
	testFilePathNotExists = "/usr/bin/not-exists-file-xxx"
	testDirPath           = "/usr/share/man/man1/"
	testDirPathNotExists  = "/usr/bin/not-exists-dir-xxx"
	testTimeout           = 1 * time.Minute
	oneTestTimeout        = 3 * time.Second
)

// TestClientCreation tests basic client creation and connection
func TestClientCreation(t *testing.T) {
	client, err := cli.NewClient()
	require.NoError(t, err, "Failed to create client")
	defer client.Close()

	// Verify client information
	familyName, familyID, groupID := client.GetFamilyInfo()
	assert.Equal(t, cli.FamilyName, familyName, "Family name should match")
	assert.NotZero(t, familyID, "Family ID should not be zero")
	assert.NotZero(t, groupID, "Group ID should not be zero")
}

// TestMessageReceiveTimeout tests the basic message receiving functionality
func TestMessageReceiveTimeout(t *testing.T) {
	client, err := cli.NewClientForMonitoring()
	require.NoError(t, err, "Failed to create client")
	defer client.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	msgCh, errCh := client.ReceiveMessages(ctx)

	// Just verify that channels are created and we can receive from them
	select {
	case <-ctx.Done():
		// Expected timeout
	case err := <-errCh:
		if err != nil {
			t.Logf("Received error (may be expected): %v", err)
		}
	case msg := <-msgCh:
		t.Logf("Received message: %+v", msg)
	}
}

// getTaskList returns a list of task IDs for the given PID by listing /proc/$pid/task/ directory
func getTaskList(pid int) ([]int, error) {
	taskDir := fmt.Sprintf("/proc/%d/task", pid)

	entries, err := os.ReadDir(taskDir)
	if err != nil {
		return nil, err
	}

	var tasks []int
	for _, entry := range entries {
		if entry.IsDir() {
			taskID, err := strconv.Atoi(entry.Name())
			if err != nil {
				continue // Skip non-numeric directory names
			}
			tasks = append(tasks, taskID)
		}
	}

	return tasks, nil
}

// TestAllSystemCalls tests all required system calls that should trigger netlink notifications
func TestAllSystemCalls(t *testing.T) {
	if os.Getuid() != 0 {
		t.Skip("Skipping test: requires root privileges for netlink monitoring")
	}

	// Create client
	c, err := cli.NewClientForMonitoring()
	require.NoError(t, err, "Failed to create client")
	defer c.Close()

	ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
	defer cancel()

	msgCh, errCh := c.ReceiveMessages(ctx)

	testCases := []struct {
		name        string
		syscall     func() error
		description string
		filenames   []string
	}{
		{
			name: "open",
			syscall: func() error {
				fd, err := unix.Open(testFilePath, unix.O_WRONLY, 0)
				if err != nil {
					return err
				}
				_ = unix.Close(fd)
				return nil
			},
			description: "Open file in write mode",
			filenames:   []string{testFilePath},
		},
		{
			name: "unlink",
			syscall: func() error {
				unlinkFile := testFilePath
				return unix.Unlink(unlinkFile)
			},
			description: "Remove file",
			filenames:   []string{testFilePath},
		},
		{
			name: "rmdir",
			syscall: func() error {
				dirPath := testDirPath
				return unix.Rmdir(dirPath)
			},
			description: "Remove directory",
			filenames:   []string{testDirPath},
		},
		{
			name: "mkdir",
			syscall: func() error {
				dirPath := testDirPathNotExists
				// First check that dirPath should not exist; if it exists, return an error.
				if _, err := os.Stat(dirPath); err == nil {
					return fmt.Errorf("directory should not exist before mkdir: %s", dirPath)
				}
				return unix.Mkdir(dirPath, 0755)
			},
			description: "Create directory",
			filenames:   []string{testDirPathNotExists},
		},
		{
			name: "mknod",
			syscall: func() error {
				nodePath := testFilePathNotExists
				// First check that nodePath should not exist; if it exists, return an error.
				if _, err := os.Stat(nodePath); err == nil {
					return fmt.Errorf("file should not exist before mknod: %s", nodePath)
				}
				return unix.Mknod(nodePath, unix.S_IFREG|0644, 0)
			},
			description: "Create special file node",
			filenames:   []string{testFilePathNotExists},
		},
		{
			name: "link",
			syscall: func() error {
				linkPath := testFilePath
				return unix.Link(linkPath, linkPath+".new")
			},
			description: "Create hard link",
			filenames:   []string{testFilePath + ".new"},
		},
		{
			name: "symlink",
			syscall: func() error {
				symlinkPath := testFilePath
				return unix.Symlink(symlinkPath, symlinkPath+".new")
			},
			description: "Create symbolic link",
			filenames:   []string{testFilePath + ".new"},
		},
		{
			name: "rename",
			syscall: func() error {
				oldPath := testFilePath
				newPath := testFilePath + ".new"
				return unix.Rename(oldPath, newPath)
			},
			description: "Rename file",
			filenames:   []string{testFilePath, testFilePath + ".new"},
		},
		{
			name: "truncate",
			syscall: func() error {
				return unix.Truncate(testFilePath, 0)
			},
			description: "Truncate file",
			filenames:   []string{testFilePath},
		},
		{
			name: "chmod",
			syscall: func() error {
				return unix.Chmod(testFilePath, 0644)
			},
			description: "Change file permissions",
			filenames:   []string{testFilePath},
		},
		{
			name: "chown",
			syscall: func() error {
				return unix.Chown(testFilePath, os.Getuid(), os.Getgid())
			},
			description: "Change file ownership",
			filenames:   []string{testFilePath},
		},
		{
			name: "utime",
			syscall: func() error {
				return unix.Utime(testFilePath, &unix.Utimbuf{
					Actime:  time.Now().Unix(),
					Modtime: time.Now().Unix(),
				})
			},
			description: "Change file access/modification time",
			filenames:   []string{testFilePath},
		},
		{
			name: "setxattr",
			syscall: func() error {
				return unix.Setxattr(testFilePath, "user.test", []byte("testvalue"), 0)
			},
			description: "Set extended attribute",
			filenames:   []string{testFilePath},
		},
		{
			name: "removexattr",
			syscall: func() error {
				// First set an attribute, then remove it
				return unix.Removexattr(testFilePath, "user.test")
			},
			description: "Remove extended attribute",
			filenames:   []string{testFilePath},
		},
		{
			name: "ioctl",
			syscall: func() error {
				// Open the file for setting immutable attribute
				fd, err := unix.Open(testFilePath, unix.O_RDONLY, 0)
				if err != nil {
					return err
				}
				defer unix.Close(fd)

				// Define ioctl constants for setting file flags
				const (
					FS_IMMUTABLE_FL = 0x00000010 // immutable flag
				)

				// First, get current flags to preserve other attributes
				currentFlags, err := unix.IoctlGetInt(fd, uint(unix.FS_IOC_GETFLAGS))
				if err != nil {
					return err
				}

				// Set the immutable flag while preserving other flags
				newFlags := currentFlags | int(FS_IMMUTABLE_FL)
				if err := unix.IoctlSetPointerInt(fd, uint(unix.FS_IOC_SETFLAGS), newFlags); err != nil {
					return err
				}

				return nil
			},
			description: "Set file immutable attribute using ioctl",
			filenames:   []string{testFilePath},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			// Start monitoring before performing syscall
			go func() {
				time.Sleep(10 * time.Millisecond) // Small delay to ensure monitoring is active
				err := tc.syscall()
				if err != nil {
					if errors.Is(err, unix.EROFS) {
						t.Logf("got EROFS error")
					} else {
						t.Errorf("error is not EROFS: %v", err)
					}
				}
			}()

			// Wait for notification
			messageReceived := false
			timeout := time.After(oneTestTimeout)

			for !messageReceived {
				select {
				case <-timeout:
					t.Fatalf("Timeout waiting for notification after %s syscall", tc.name)
				case err := <-errCh:
					if err != nil {
						t.Fatalf("Received error: %v after %s syscall", err, tc.name)
					}
				case msg, ok := <-msgCh:
					if !ok {
						t.Fatal("Message channel closed unexpectedly")
					}

					t.Logf("Received message: Files=%v, PID=%d, PPID=%d, UID=%d, GID=%d, Comm=%s",
						msg.Filenames, msg.PID, msg.PPID, msg.UID, msg.GID, msg.Comm)

					if len(msg.Filenames) > 0 {
						messageReceived = true
						// Check if this message is related to our test files
						tasks, err := getTaskList(os.Getpid())
						if err != nil {
							t.Logf("Failed to get task list: %v", err)
						}

						// Verify message fields
						assert.Contains(t, tasks, int(msg.PID), "PID should be in task list")
						assert.NotEmpty(t, msg.Comm, "Comm should not be empty")
						assert.NotZero(t, msg.PID, "PID should not be zero")
						assert.NotZero(t, msg.PPID, "PPID should not be zero")
						assert.Zero(t, msg.UID, "UID should be zero")
						assert.Zero(t, msg.GID, "GID should be zero")

						t.Logf("✓ Successfully received notification for %s syscall (%s)", tc.name, tc.description)
						t.Logf("  Process Info:")
						t.Logf("    PID: %d, PPID: %d", msg.PID, msg.PPID)
						t.Logf("    UID: %d, GID: %d", msg.UID, msg.GID)
						t.Logf("    Comm: %s", msg.Comm)
						t.Logf("  Error Info:")
						t.Logf("    Filenames: %v", msg.Filenames)
					}
				}
			}
		})
	}
}
