package container

import (
	"context"
	"os"
	"os/exec"
	"path/filepath"
	"testing"
	"time"

	"github.com/host-uk/core/pkg/io"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// MockHypervisor is a mock implementation for testing.
type MockHypervisor struct {
	name         string
	available    bool
	buildErr     error
	lastImage    string
	lastOpts     *HypervisorOptions
	commandToRun string
}

func NewMockHypervisor() *MockHypervisor {
	return &MockHypervisor{
		name:         "mock",
		available:    true,
		commandToRun: "echo",
	}
}

func (m *MockHypervisor) Name() string {
	return m.name
}

func (m *MockHypervisor) Available() bool {
	return m.available
}

func (m *MockHypervisor) BuildCommand(ctx context.Context, image string, opts *HypervisorOptions) (*exec.Cmd, error) {
	m.lastImage = image
	m.lastOpts = opts
	if m.buildErr != nil {
		return nil, m.buildErr
	}
	// Return a simple command that exits quickly
	return exec.CommandContext(ctx, m.commandToRun, "test"), nil
}

// newTestManager creates a LinuxKitManager with mock hypervisor for testing.
// Uses manual temp directory management to avoid race conditions with t.TempDir cleanup.
func newTestManager(t *testing.T) (*LinuxKitManager, *MockHypervisor, string) {
	tmpDir, err := os.MkdirTemp("", "linuxkit-test-*")
	require.NoError(t, err)

	// Manual cleanup that handles race conditions with state file writes
	t.Cleanup(func() {
		// Give any pending file operations time to complete
		time.Sleep(10 * time.Millisecond)
		_ = os.RemoveAll(tmpDir)
	})

	statePath := filepath.Join(tmpDir, "containers.json")

	state, err := LoadState(io.Local, statePath)
	require.NoError(t, err)

	mock := NewMockHypervisor()
	manager := NewLinuxKitManagerWithHypervisor(io.Local, state, mock)

	return manager, mock, tmpDir
}

func TestNewLinuxKitManagerWithHypervisor_Good(t *testing.T) {
	tmpDir := t.TempDir()
	statePath := filepath.Join(tmpDir, "containers.json")
	state, _ := LoadState(io.Local, statePath)
	mock := NewMockHypervisor()

	manager := NewLinuxKitManagerWithHypervisor(io.Local, state, mock)

	assert.NotNil(t, manager)
	assert.Equal(t, state, manager.State())
	assert.Equal(t, mock, manager.Hypervisor())
}

func TestLinuxKitManager_Run_Good_Detached(t *testing.T) {
	manager, mock, tmpDir := newTestManager(t)

	// Create a test image file
	imagePath := filepath.Join(tmpDir, "test.iso")
	err := os.WriteFile(imagePath, []byte("fake image"), 0644)
	require.NoError(t, err)

	// Use a command that runs briefly then exits
	mock.commandToRun = "sleep"

	ctx := context.Background()
	opts := RunOptions{
		Name:   "test-vm",
		Detach: true,
		Memory: 512,
		CPUs:   2,
	}

	container, err := manager.Run(ctx, imagePath, opts)
	require.NoError(t, err)

	assert.NotEmpty(t, container.ID)
	assert.Equal(t, "test-vm", container.Name)
	assert.Equal(t, imagePath, container.Image)
	assert.Equal(t, StatusRunning, container.Status)
	assert.Greater(t, container.PID, 0)
	assert.Equal(t, 512, container.Memory)
	assert.Equal(t, 2, container.CPUs)

	// Verify hypervisor was called with correct options
	assert.Equal(t, imagePath, mock.lastImage)
	assert.Equal(t, 512, mock.lastOpts.Memory)
	assert.Equal(t, 2, mock.lastOpts.CPUs)

	// Clean up - stop the container
	time.Sleep(100 * time.Millisecond)
}

func TestLinuxKitManager_Run_Good_DefaultValues(t *testing.T) {
	manager, mock, tmpDir := newTestManager(t)

	imagePath := filepath.Join(tmpDir, "test.qcow2")
	err := os.WriteFile(imagePath, []byte("fake image"), 0644)
	require.NoError(t, err)

	ctx := context.Background()
	opts := RunOptions{Detach: true}

	container, err := manager.Run(ctx, imagePath, opts)
	require.NoError(t, err)

	// Check defaults were applied
	assert.Equal(t, 1024, mock.lastOpts.Memory)
	assert.Equal(t, 1, mock.lastOpts.CPUs)
	assert.Equal(t, 2222, mock.lastOpts.SSHPort)

	// Name should default to first 8 chars of ID
	assert.Equal(t, container.ID[:8], container.Name)

	// Wait for the mock process to complete to avoid temp dir cleanup issues
	time.Sleep(50 * time.Millisecond)
}

func TestLinuxKitManager_Run_Bad_ImageNotFound(t *testing.T) {
	manager, _, _ := newTestManager(t)

	ctx := context.Background()
	opts := RunOptions{Detach: true}

	_, err := manager.Run(ctx, "/nonexistent/image.iso", opts)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "image not found")
}

func TestLinuxKitManager_Run_Bad_UnsupportedFormat(t *testing.T) {
	manager, _, tmpDir := newTestManager(t)

	imagePath := filepath.Join(tmpDir, "test.txt")
	err := os.WriteFile(imagePath, []byte("not an image"), 0644)
	require.NoError(t, err)

	ctx := context.Background()
	opts := RunOptions{Detach: true}

	_, err = manager.Run(ctx, imagePath, opts)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "unsupported image format")
}

func TestLinuxKitManager_Stop_Good(t *testing.T) {
	manager, _, _ := newTestManager(t)

	// Add a fake running container with a non-existent PID
	// The Stop function should handle this gracefully
	container := &Container{
		ID:        "abc12345",
		Status:    StatusRunning,
		PID:       999999, // Non-existent PID
		StartedAt: time.Now(),
	}
	_ = manager.State().Add(container)

	ctx := context.Background()
	err := manager.Stop(ctx, "abc12345")

	// Stop should succeed (process doesn't exist, so container is marked stopped)
	assert.NoError(t, err)

	// Verify the container status was updated
	c, ok := manager.State().Get("abc12345")
	assert.True(t, ok)
	assert.Equal(t, StatusStopped, c.Status)
}

func TestLinuxKitManager_Stop_Bad_NotFound(t *testing.T) {
	manager, _, _ := newTestManager(t)

	ctx := context.Background()
	err := manager.Stop(ctx, "nonexistent")

	assert.Error(t, err)
	assert.Contains(t, err.Error(), "container not found")
}

func TestLinuxKitManager_Stop_Bad_NotRunning(t *testing.T) {
	_, _, tmpDir := newTestManager(t)
	statePath := filepath.Join(tmpDir, "containers.json")
	state, err := LoadState(io.Local, statePath)
	require.NoError(t, err)
	manager := NewLinuxKitManagerWithHypervisor(io.Local, state, NewMockHypervisor())

	container := &Container{
		ID:     "abc12345",
		Status: StatusStopped,
	}
	_ = state.Add(container)

	ctx := context.Background()
	err = manager.Stop(ctx, "abc12345")

	assert.Error(t, err)
	assert.Contains(t, err.Error(), "not running")
}

func TestLinuxKitManager_List_Good(t *testing.T) {
	_, _, tmpDir := newTestManager(t)
	statePath := filepath.Join(tmpDir, "containers.json")
	state, err := LoadState(io.Local, statePath)
	require.NoError(t, err)
	manager := NewLinuxKitManagerWithHypervisor(io.Local, state, NewMockHypervisor())

	_ = state.Add(&Container{ID: "aaa11111", Status: StatusStopped})
	_ = state.Add(&Container{ID: "bbb22222", Status: StatusStopped})

	ctx := context.Background()
	containers, err := manager.List(ctx)

	require.NoError(t, err)
	assert.Len(t, containers, 2)
}

func TestLinuxKitManager_List_Good_VerifiesRunningStatus(t *testing.T) {
	_, _, tmpDir := newTestManager(t)
	statePath := filepath.Join(tmpDir, "containers.json")
	state, err := LoadState(io.Local, statePath)
	require.NoError(t, err)
	manager := NewLinuxKitManagerWithHypervisor(io.Local, state, NewMockHypervisor())

	// Add a "running" container with a fake PID that doesn't exist
	_ = state.Add(&Container{
		ID:     "abc12345",
		Status: StatusRunning,
		PID:    999999, // PID that almost certainly doesn't exist
	})

	ctx := context.Background()
	containers, err := manager.List(ctx)

	require.NoError(t, err)
	assert.Len(t, containers, 1)
	// Status should have been updated to stopped since PID doesn't exist
	assert.Equal(t, StatusStopped, containers[0].Status)
}

func TestLinuxKitManager_Logs_Good(t *testing.T) {
	manager, _, tmpDir := newTestManager(t)

	// Create a log file manually
	logsDir := filepath.Join(tmpDir, "logs")
	require.NoError(t, os.MkdirAll(logsDir, 0755))

	container := &Container{ID: "abc12345"}
	_ = manager.State().Add(container)

	// Override the default logs dir for testing by creating the log file
	// at the expected location
	logContent := "test log content\nline 2\n"
	logPath, err := LogPath("abc12345")
	require.NoError(t, err)
	require.NoError(t, os.MkdirAll(filepath.Dir(logPath), 0755))
	require.NoError(t, os.WriteFile(logPath, []byte(logContent), 0644))

	ctx := context.Background()
	reader, err := manager.Logs(ctx, "abc12345", false)

	require.NoError(t, err)
	defer func() { _ = reader.Close() }()

	buf := make([]byte, 1024)
	n, _ := reader.Read(buf)
	assert.Equal(t, logContent, string(buf[:n]))
}

func TestLinuxKitManager_Logs_Bad_NotFound(t *testing.T) {
	manager, _, _ := newTestManager(t)

	ctx := context.Background()
	_, err := manager.Logs(ctx, "nonexistent", false)

	assert.Error(t, err)
	assert.Contains(t, err.Error(), "container not found")
}

func TestLinuxKitManager_Logs_Bad_NoLogFile(t *testing.T) {
	manager, _, _ := newTestManager(t)

	// Use a unique ID that won't have a log file
	uniqueID, err := GenerateID()
	require.NoError(t, err)
	container := &Container{ID: uniqueID}
	_ = manager.State().Add(container)

	ctx := context.Background()
	reader, err := manager.Logs(ctx, uniqueID, false)

	// If logs existed somehow, clean up the reader
	if reader != nil {
		_ = reader.Close()
	}

	assert.Error(t, err)
	if err != nil {
		assert.Contains(t, err.Error(), "no logs available")
	}
}

func TestLinuxKitManager_Exec_Bad_NotFound(t *testing.T) {
	manager, _, _ := newTestManager(t)

	ctx := context.Background()
	err := manager.Exec(ctx, "nonexistent", []string{"ls"})

	assert.Error(t, err)
	assert.Contains(t, err.Error(), "container not found")
}

func TestLinuxKitManager_Exec_Bad_NotRunning(t *testing.T) {
	manager, _, _ := newTestManager(t)

	container := &Container{ID: "abc12345", Status: StatusStopped}
	_ = manager.State().Add(container)

	ctx := context.Background()
	err := manager.Exec(ctx, "abc12345", []string{"ls"})

	assert.Error(t, err)
	assert.Contains(t, err.Error(), "not running")
}

func TestDetectImageFormat_Good(t *testing.T) {
	tests := []struct {
		path   string
		format ImageFormat
	}{
		{"/path/to/image.iso", FormatISO},
		{"/path/to/image.ISO", FormatISO},
		{"/path/to/image.qcow2", FormatQCOW2},
		{"/path/to/image.QCOW2", FormatQCOW2},
		{"/path/to/image.vmdk", FormatVMDK},
		{"/path/to/image.raw", FormatRaw},
		{"/path/to/image.img", FormatRaw},
		{"image.iso", FormatISO},
	}

	for _, tt := range tests {
		t.Run(tt.path, func(t *testing.T) {
			assert.Equal(t, tt.format, DetectImageFormat(tt.path))
		})
	}
}

func TestDetectImageFormat_Bad_Unknown(t *testing.T) {
	tests := []string{
		"/path/to/image.txt",
		"/path/to/image",
		"noextension",
		"/path/to/image.docx",
	}

	for _, path := range tests {
		t.Run(path, func(t *testing.T) {
			assert.Equal(t, FormatUnknown, DetectImageFormat(path))
		})
	}
}

func TestQemuHypervisor_Name_Good(t *testing.T) {
	q := NewQemuHypervisor()
	assert.Equal(t, "qemu", q.Name())
}

func TestQemuHypervisor_BuildCommand_Good(t *testing.T) {
	q := NewQemuHypervisor()

	ctx := context.Background()
	opts := &HypervisorOptions{
		Memory:  2048,
		CPUs:    4,
		SSHPort: 2222,
		Ports:   map[int]int{8080: 80},
		Detach:  true,
	}

	cmd, err := q.BuildCommand(ctx, "/path/to/image.iso", opts)
	require.NoError(t, err)
	assert.NotNil(t, cmd)

	// Check command path
	assert.Contains(t, cmd.Path, "qemu")

	// Check that args contain expected values
	args := cmd.Args
	assert.Contains(t, args, "-m")
	assert.Contains(t, args, "2048")
	assert.Contains(t, args, "-smp")
	assert.Contains(t, args, "4")
	assert.Contains(t, args, "-nographic")
}

func TestLinuxKitManager_Logs_Good_Follow(t *testing.T) {
	manager, _, _ := newTestManager(t)

	// Create a unique container ID
	uniqueID, err := GenerateID()
	require.NoError(t, err)
	container := &Container{ID: uniqueID}
	_ = manager.State().Add(container)

	// Create a log file at the expected location
	logPath, err := LogPath(uniqueID)
	require.NoError(t, err)
	require.NoError(t, os.MkdirAll(filepath.Dir(logPath), 0755))

	// Write initial content
	err = os.WriteFile(logPath, []byte("initial log content\n"), 0644)
	require.NoError(t, err)

	// Create a cancellable context
	ctx, cancel := context.WithCancel(context.Background())

	// Get the follow reader
	reader, err := manager.Logs(ctx, uniqueID, true)
	require.NoError(t, err)

	// Cancel the context to stop the follow
	cancel()

	// Read should return EOF after context cancellation
	buf := make([]byte, 1024)
	_, readErr := reader.Read(buf)
	// After context cancel, Read should return EOF
	assert.Equal(t, "EOF", readErr.Error())

	// Close the reader
	assert.NoError(t, reader.Close())
}

func TestFollowReader_Read_Good_WithData(t *testing.T) {
	tmpDir := t.TempDir()
	logPath := filepath.Join(tmpDir, "test.log")

	// Create log file with content
	content := "test log line 1\ntest log line 2\n"
	err := os.WriteFile(logPath, []byte(content), 0644)
	require.NoError(t, err)

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

	reader, err := newFollowReader(ctx, io.Local, logPath)
	require.NoError(t, err)
	defer func() { _ = reader.Close() }()

	// The followReader seeks to end, so we need to append more content
	f, err := os.OpenFile(logPath, os.O_APPEND|os.O_WRONLY, 0644)
	require.NoError(t, err)
	_, err = f.WriteString("new line\n")
	require.NoError(t, err)
	require.NoError(t, f.Close())

	// Give the reader time to poll
	time.Sleep(150 * time.Millisecond)

	buf := make([]byte, 1024)
	n, err := reader.Read(buf)
	if err == nil {
		assert.Greater(t, n, 0)
	}
}

func TestFollowReader_Read_Good_ContextCancel(t *testing.T) {
	tmpDir := t.TempDir()
	logPath := filepath.Join(tmpDir, "test.log")

	// Create log file
	err := os.WriteFile(logPath, []byte("initial content\n"), 0644)
	require.NoError(t, err)

	ctx, cancel := context.WithCancel(context.Background())

	reader, err := newFollowReader(ctx, io.Local, logPath)
	require.NoError(t, err)

	// Cancel the context
	cancel()

	// Read should return EOF
	buf := make([]byte, 1024)
	_, readErr := reader.Read(buf)
	assert.Equal(t, "EOF", readErr.Error())

	_ = reader.Close()
}

func TestFollowReader_Close_Good(t *testing.T) {
	tmpDir := t.TempDir()
	logPath := filepath.Join(tmpDir, "test.log")

	err := os.WriteFile(logPath, []byte("content\n"), 0644)
	require.NoError(t, err)

	ctx := context.Background()
	reader, err := newFollowReader(ctx, io.Local, logPath)
	require.NoError(t, err)

	err = reader.Close()
	assert.NoError(t, err)

	// Reading after close should fail or return EOF
	buf := make([]byte, 1024)
	_, readErr := reader.Read(buf)
	assert.Error(t, readErr)
}

func TestNewFollowReader_Bad_FileNotFound(t *testing.T) {
	ctx := context.Background()
	_, err := newFollowReader(ctx, io.Local, "/nonexistent/path/to/file.log")

	assert.Error(t, err)
}

func TestLinuxKitManager_Run_Bad_BuildCommandError(t *testing.T) {
	manager, mock, tmpDir := newTestManager(t)

	// Create a test image file
	imagePath := filepath.Join(tmpDir, "test.iso")
	err := os.WriteFile(imagePath, []byte("fake image"), 0644)
	require.NoError(t, err)

	// Configure mock to return an error
	mock.buildErr = assert.AnError

	ctx := context.Background()
	opts := RunOptions{Detach: true}

	_, err = manager.Run(ctx, imagePath, opts)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "failed to build hypervisor command")
}

func TestLinuxKitManager_Run_Good_Foreground(t *testing.T) {
	manager, mock, tmpDir := newTestManager(t)

	// Create a test image file
	imagePath := filepath.Join(tmpDir, "test.iso")
	err := os.WriteFile(imagePath, []byte("fake image"), 0644)
	require.NoError(t, err)

	// Use echo which exits quickly
	mock.commandToRun = "echo"

	ctx := context.Background()
	opts := RunOptions{
		Name:   "test-foreground",
		Detach: false, // Run in foreground
		Memory: 512,
		CPUs:   1,
	}

	container, err := manager.Run(ctx, imagePath, opts)
	require.NoError(t, err)

	assert.NotEmpty(t, container.ID)
	assert.Equal(t, "test-foreground", container.Name)
	// Foreground process should have completed
	assert.Equal(t, StatusStopped, container.Status)
}

func TestLinuxKitManager_Stop_Good_ContextCancelled(t *testing.T) {
	manager, mock, tmpDir := newTestManager(t)

	// Create a test image file
	imagePath := filepath.Join(tmpDir, "test.iso")
	err := os.WriteFile(imagePath, []byte("fake image"), 0644)
	require.NoError(t, err)

	// Use a command that takes a long time
	mock.commandToRun = "sleep"

	// Start a container
	ctx := context.Background()
	opts := RunOptions{
		Name:   "test-cancel",
		Detach: true,
	}

	container, err := manager.Run(ctx, imagePath, opts)
	require.NoError(t, err)

	// Ensure cleanup happens regardless of test outcome
	t.Cleanup(func() {
		_ = manager.Stop(context.Background(), container.ID)
	})

	// Create a context that's already cancelled
	cancelCtx, cancel := context.WithCancel(context.Background())
	cancel()

	// Stop with cancelled context
	err = manager.Stop(cancelCtx, container.ID)
	// Should return context error
	assert.Error(t, err)
	assert.Equal(t, context.Canceled, err)
}

func TestIsProcessRunning_Good_ExistingProcess(t *testing.T) {
	// Use our own PID which definitely exists
	running := isProcessRunning(os.Getpid())
	assert.True(t, running)
}

func TestIsProcessRunning_Bad_NonexistentProcess(t *testing.T) {
	// Use a PID that almost certainly doesn't exist
	running := isProcessRunning(999999)
	assert.False(t, running)
}

func TestLinuxKitManager_Run_Good_WithPortsAndVolumes(t *testing.T) {
	manager, mock, tmpDir := newTestManager(t)

	imagePath := filepath.Join(tmpDir, "test.iso")
	err := os.WriteFile(imagePath, []byte("fake image"), 0644)
	require.NoError(t, err)

	ctx := context.Background()
	opts := RunOptions{
		Name:    "test-ports",
		Detach:  true,
		Memory:  512,
		CPUs:    1,
		SSHPort: 2223,
		Ports:   map[int]int{8080: 80, 443: 443},
		Volumes: map[string]string{"/host/data": "/container/data"},
	}

	container, err := manager.Run(ctx, imagePath, opts)
	require.NoError(t, err)

	assert.NotEmpty(t, container.ID)
	assert.Equal(t, map[int]int{8080: 80, 443: 443}, container.Ports)
	assert.Equal(t, 2223, mock.lastOpts.SSHPort)
	assert.Equal(t, map[string]string{"/host/data": "/container/data"}, mock.lastOpts.Volumes)

	time.Sleep(50 * time.Millisecond)
}

func TestFollowReader_Read_Bad_ReaderError(t *testing.T) {
	tmpDir := t.TempDir()
	logPath := filepath.Join(tmpDir, "test.log")

	// Create log file
	err := os.WriteFile(logPath, []byte("content\n"), 0644)
	require.NoError(t, err)

	ctx := context.Background()
	reader, err := newFollowReader(ctx, io.Local, logPath)
	require.NoError(t, err)

	// Close the underlying file to cause read errors
	_ = reader.file.Close()

	// Read should return an error
	buf := make([]byte, 1024)
	_, readErr := reader.Read(buf)
	assert.Error(t, readErr)
}

func TestLinuxKitManager_Run_Bad_StartError(t *testing.T) {
	manager, mock, tmpDir := newTestManager(t)

	imagePath := filepath.Join(tmpDir, "test.iso")
	err := os.WriteFile(imagePath, []byte("fake image"), 0644)
	require.NoError(t, err)

	// Use a command that doesn't exist to cause Start() to fail
	mock.commandToRun = "/nonexistent/command/that/does/not/exist"

	ctx := context.Background()
	opts := RunOptions{
		Name:   "test-start-error",
		Detach: true,
	}

	_, err = manager.Run(ctx, imagePath, opts)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "failed to start VM")
}

func TestLinuxKitManager_Run_Bad_ForegroundStartError(t *testing.T) {
	manager, mock, tmpDir := newTestManager(t)

	imagePath := filepath.Join(tmpDir, "test.iso")
	err := os.WriteFile(imagePath, []byte("fake image"), 0644)
	require.NoError(t, err)

	// Use a command that doesn't exist to cause Start() to fail
	mock.commandToRun = "/nonexistent/command/that/does/not/exist"

	ctx := context.Background()
	opts := RunOptions{
		Name:   "test-foreground-error",
		Detach: false,
	}

	_, err = manager.Run(ctx, imagePath, opts)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "failed to start VM")
}

func TestLinuxKitManager_Run_Good_ForegroundWithError(t *testing.T) {
	manager, mock, tmpDir := newTestManager(t)

	imagePath := filepath.Join(tmpDir, "test.iso")
	err := os.WriteFile(imagePath, []byte("fake image"), 0644)
	require.NoError(t, err)

	// Use a command that exits with error
	mock.commandToRun = "false" // false command exits with code 1

	ctx := context.Background()
	opts := RunOptions{
		Name:   "test-foreground-exit-error",
		Detach: false,
	}

	container, err := manager.Run(ctx, imagePath, opts)
	require.NoError(t, err) // Run itself should succeed

	// Container should be in error state since process exited with error
	assert.Equal(t, StatusError, container.Status)
}

func TestLinuxKitManager_Stop_Good_ProcessExitedWhileRunning(t *testing.T) {
	manager, _, _ := newTestManager(t)

	// Add a "running" container with a process that has already exited
	// This simulates the race condition where process exits between status check
	// and signal send
	container := &Container{
		ID:        "test1234",
		Status:    StatusRunning,
		PID:       999999, // Non-existent PID
		StartedAt: time.Now(),
	}
	_ = manager.State().Add(container)

	ctx := context.Background()
	err := manager.Stop(ctx, "test1234")

	// Stop should succeed gracefully
	assert.NoError(t, err)

	// Container should be stopped
	c, ok := manager.State().Get("test1234")
	assert.True(t, ok)
	assert.Equal(t, StatusStopped, c.Status)
}
