package vmm

import (
	"context"
	"errors"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/openeuler/Conch/internal/config"
	"github.com/openeuler/Conch/internal/vmm/driver"
)

func TestSandboxSocketPathUsesShortStableName(t *testing.T) {
	oldWorkDir := config.WorkDir
	t.Cleanup(func() { config.WorkDir = oldWorkDir })
	config.WorkDir = filepath.Join(os.TempDir(), "conch-socket-test-"+strings.Repeat("a", 45))
	t.Cleanup(func() { _ = os.RemoveAll(config.WorkDir) })

	sandboxID := "sandbox-" + strings.Repeat("very-long-id-", 12)
	got, err := SandboxSocketPath("x", sandboxID)
	if err != nil {
		t.Fatalf("SandboxSocketPath() error = %v", err)
	}
	if len(got) > unixSocketPathMax {
		t.Fatalf("socket path length = %d, want <= %d: %s", len(got), unixSocketPathMax, got)
	}
	if strings.Contains(got, sandboxID) {
		t.Fatalf("socket path still embeds sandbox id: %s", got)
	}
	again, err := SandboxSocketPath("x", sandboxID)
	if err != nil {
		t.Fatalf("SandboxSocketPath() second error = %v", err)
	}
	if got != again {
		t.Fatalf("SandboxSocketPath() is not stable: %s vs %s", got, again)
	}
}

func TestSandboxSocketPathRejectsTooLongWorkDir(t *testing.T) {
	oldWorkDir := config.WorkDir
	t.Cleanup(func() { config.WorkDir = oldWorkDir })
	config.WorkDir = filepath.Join(os.TempDir(), "conch-socket-test-"+strings.Repeat("a", 110))
	t.Cleanup(func() { _ = os.RemoveAll(config.WorkDir) })

	if _, err := SandboxSocketPath("x", "sandbox"); err == nil {
		t.Fatalf("SandboxSocketPath() error = nil, want path length error")
	}
}

func TestIsSandboxSocketName(t *testing.T) {
	for _, name := range []string{
		"0123456789abcdef.sock",
		"0123456789abcdef.sock.serial",
	} {
		if !sandboxSocketNameRE.MatchString(name) {
			t.Fatalf("sandboxSocketNameRE.MatchString(%q) = false, want true", name)
		}
	}
	for _, name := range []string{
		"sandbox.sock",
		"0123456789abcdef.serial",
		"0123456789abcdef.sock.bak",
		"0123456789ABCDEf.sock",
	} {
		if sandboxSocketNameRE.MatchString(name) {
			t.Fatalf("sandboxSocketNameRE.MatchString(%q) = true, want false", name)
		}
	}
}

func TestKillStaleVMMProcessIgnoresReusedPID(t *testing.T) {
	binaries := map[string]string{"test-vmm": "/opt/test/vmm"}
	if err := killStaleVMMProcess(os.Getpid(), binaries); err != nil {
		t.Fatalf("killStaleVMMProcess() error = %v, want nil for non-VMM process", err)
	}
}

func TestMatchesConfiguredVMMCommand(t *testing.T) {
	binaries := map[string]string{
		"vmm-a": "/opt/test/vmm-a",
		"vmm-b": "/opt/test/vmm-b",
	}
	tests := []struct {
		name    string
		cmdline string
		want    bool
	}{
		{name: "configured binary", cmdline: "/opt/test/vmm-a\x00--api-socket\x00socket", want: true},
		{name: "second configured binary", cmdline: "/opt/test/vmm-b\x00--restore", want: true},
		{name: "binary only in argument", cmdline: "/usr/bin/wrapper\x00/opt/test/vmm-a", want: false},
		{name: "path prefix", cmdline: "/opt/test/vmm-a.backup\x00--run", want: false},
		{name: "empty command line", cmdline: "", want: false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := matchesConfiguredVMMCommand([]byte(tt.cmdline), binaries); got != tt.want {
				t.Fatalf("matchesConfiguredVMMCommand(%q) = %t, want %t", tt.cmdline, got, tt.want)
			}
		})
	}
}

type blockingDaemonClient struct {
	release      chan struct{}
	cleanupCalls atomic.Int32
	deleteCalls  atomic.Int32
}

func (c *blockingDaemonClient) BuildStartCmd(*ResourceArgs, bool) (string, error) { return "", nil }
func (c *blockingDaemonClient) CheckAgentAlive(ctx context.Context, processExited driver.ProcessExit) error {
	select {
	case <-c.release:
		return nil
	case <-processExited.Done():
		if waitErr := processExited.Err(); waitErr == nil {
			return errors.New("vmm process exited before conch-init became ready")
		} else {
			return errors.Join(errors.New("vmm process exited before conch-init became ready"), waitErr)
		}
	case <-ctx.Done():
		return ctx.Err()
	}
}
func (c *blockingDaemonClient) PauseVM() error                  { return nil }
func (c *blockingDaemonClient) ResumeVM() error                 { return nil }
func (c *blockingDaemonClient) DeleteVM() error                 { c.deleteCalls.Add(1); return nil }
func (c *blockingDaemonClient) CreateSnapshot(string) error     { return nil }
func (c *blockingDaemonClient) LoadSnapshot(string, bool) error { return nil }
func (c *blockingDaemonClient) PrepareLaunch(*ResourceArgs, bool) error {
	return nil
}
func (c *blockingDaemonClient) AfterProcessStart() {}
func (c *blockingDaemonClient) WaitForCreateReady(context.Context, driver.ProcessExit) error {
	return nil
}
func (c *blockingDaemonClient) WaitForRestoreReady(context.Context, driver.ProcessExit) error {
	return nil
}
func (c *blockingDaemonClient) Cleanup() { c.cleanupCalls.Add(1) }

func TestStopIgnoresProcessDoneWhenProcessAlreadyFinished(t *testing.T) {
	cmd := exec.Command("true")
	if err := cmd.Start(); err != nil {
		t.Fatalf("start process: %v", err)
	}
	if err := cmd.Wait(); err != nil {
		t.Fatalf("wait for process: %v", err)
	}

	client := &blockingDaemonClient{release: make(chan struct{})}
	process := &Process{
		cmd:      cmd,
		adapter:  client,
		exitDone: make(chan struct{}),
	}

	if err := process.Stop(context.Background()); err != nil {
		t.Fatalf("Stop() error = %v, want nil", err)
	}
	if got := client.cleanupCalls.Load(); got != 1 {
		t.Fatalf("Cleanup() calls = %d, want 1", got)
	}
}

func TestWaitForAgentAliveReturnsProcessExitError(t *testing.T) {
	processErr := errors.New("stratovirt exited after creating qmp socket")
	client := &blockingDaemonClient{release: make(chan struct{})}
	process := &Process{
		adapter:  client,
		exitDone: make(chan struct{}),
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	process.recordExit(processErr)
	t.Cleanup(func() { close(client.release) })

	err := process.waitForAgentAlive(ctx)
	if !errors.Is(err, processErr) {
		t.Fatalf("waitForAgentAlive() error = %v, want %v", err, processErr)
	}
	if !strings.Contains(err.Error(), "exited before conch-init became ready") {
		t.Fatalf("waitForAgentAlive() error = %q, want early exit context", err.Error())
	}
}

func TestStopRetainsResourcesUntilExit(t *testing.T) {
	cmd := exec.Command("sleep", "30")
	if err := cmd.Start(); err != nil {
		t.Fatal(err)
	}
	defer func() { _ = cmd.Process.Kill(); _ = cmd.Wait() }()
	client := &blockingDaemonClient{}
	socket := filepath.Join(t.TempDir(), "control")
	if err := os.WriteFile(socket, nil, 0600); err != nil {
		t.Fatal(err)
	}
	process := &Process{cmd: cmd, adapter: client, exitDone: make(chan struct{}), apiReady: true, VmmSocketPath: socket}
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	// The reaper has not confirmed exit, even if SIGTERM has been delivered.
	if err := process.Stop(ctx); !errors.Is(err, context.Canceled) {
		t.Fatal(err)
	}
	if client.deleteCalls.Load() != 1 {
		t.Fatal("stop did not call control API")
	}
	if client.cleanupCalls.Load() != 0 {
		t.Fatal("cleaned resources before exit")
	}
	close(process.exitDone)
	if err := process.Stop(context.Background()); err != nil {
		t.Fatal(err)
	}
	if client.cleanupCalls.Load() != 1 {
		t.Fatal("resources were not cleaned after exit")
	}
}