//  Copyright 2024 Google LLC
//
//  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
//
//     https://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"
	"os"
	"path/filepath"
	"runtime"
	"testing"
	"time"

	acmpb "github.com/GoogleCloudPlatform/google-guest-agent/internal/acp/proto/google_guest_agent/acp"
	"github.com/GoogleCloudPlatform/google-guest-agent/internal/acs/client"
	"github.com/GoogleCloudPlatform/google-guest-agent/internal/cfg"
	"github.com/GoogleCloudPlatform/google-guest-agent/internal/utils/file"
)

func createTestFile(t *testing.T, file string) {
	t.Helper()
	if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil {
		t.Fatalf("os.MkdirAll(%s) failed unexpectedly: %v", filepath.Dir(file), err)
	}
	f, err := os.Create(file)
	if err != nil {
		t.Fatalf("os.Create(%s) failed unexpectedly: %v", file, err)
	}
	f.Close()
}

// setupPlugin sets up a plugin for testing StopPlugin step.
func setupPlugin(ctx context.Context, t *testing.T, plugin *Plugin, ts *testPluginServer) {
	t.Helper()
	startTestServer(t, ts, "unix", plugin.Address)
	if err := plugin.Connect(ctx); err != nil {
		t.Fatalf("plugin.Connect(ctx) failed unexpectedly: %v", err)
	}
	entryPath := filepath.Join(t.TempDir(), "testplugin.txt")
	createTestFile(t, entryPath)
	stateFile := plugin.stateFile()
	createTestFile(t, stateFile)
	plugin.InstallPath = filepath.Dir(entryPath)
	plugin.EntryPath = entryPath
	if err := os.MkdirAll(plugin.stateDir(), 0755); err != nil {
		t.Fatalf("os.MkdirAll(%s) failed unexpectedly: %v", plugin.stateDir(), err)
	}
}

func TestStopPlugin(t *testing.T) {
	ctc := setupConstraintTestClient(t)
	stateDir := t.TempDir()
	setBaseStateDir(t, stateDir)
	cfg.Retrieve().Core.ACSClient = false
	addr := filepath.Join(t.TempDir(), "pluginA_revisionA.sock")

	tests := []struct {
		name           string
		stopCleanup    bool
		psClient       *mockPsClient
		wantStopRPC    bool
		reusePid       bool
		installCleanup bool
		plugin         *Plugin
	}{
		{
			name:           "stop_cleanup_true",
			stopCleanup:    true,
			psClient:       &mockPsClient{alive: true},
			wantStopRPC:    true,
			installCleanup: true,
			plugin:         &Plugin{Name: "testplugin1", EntryPath: "testplugin1", Revision: "1", Protocol: "unix", Address: addr, Manifest: &Manifest{StartAttempts: 3, StopTimeout: time.Second * 3, PluginInstallationType: acmpb.PluginInstallationType_DYNAMIC_INSTALLATION}, RuntimeInfo: &RuntimeInfo{Pid: -5555}},
		},
		{
			name:           "stop_cleanup_false",
			stopCleanup:    false,
			installCleanup: false,
			psClient:       &mockPsClient{alive: false},
			wantStopRPC:    false,
			plugin:         &Plugin{Name: "testplugin2", EntryPath: "testplugin2", Revision: "1", Protocol: "unix", Address: addr, Manifest: &Manifest{StartAttempts: 3, StopTimeout: time.Second * 3, PluginInstallationType: acmpb.PluginInstallationType_DYNAMIC_INSTALLATION}, RuntimeInfo: &RuntimeInfo{Pid: -5555}},
		},
		{
			name:           "core_stop_cleanup_true",
			stopCleanup:    true,
			psClient:       &mockPsClient{alive: true},
			wantStopRPC:    true,
			installCleanup: false,
			plugin:         &Plugin{Name: "testplugin3", EntryPath: "testplugin3", Revision: "1", Protocol: "unix", Address: addr, Manifest: &Manifest{StartAttempts: 3, StopTimeout: time.Second * 3, PluginInstallationType: acmpb.PluginInstallationType_LOCAL_INSTALLATION}, RuntimeInfo: &RuntimeInfo{Pid: -5555}},
		},
		{
			name:           "core_stop_cleanup_false",
			stopCleanup:    false,
			installCleanup: false,
			psClient:       &mockPsClient{alive: true},
			wantStopRPC:    true,
			plugin:         &Plugin{Name: "testplugin4", EntryPath: "testplugin4", Revision: "1", Protocol: "unix", Address: addr, Manifest: &Manifest{StartAttempts: 3, StopTimeout: time.Second * 3, PluginInstallationType: acmpb.PluginInstallationType_LOCAL_INSTALLATION}, RuntimeInfo: &RuntimeInfo{Pid: -5555}},
		},
		{
			name:           "nokill_cleanup_true",
			stopCleanup:    true,
			psClient:       &mockPsClient{alive: true},
			wantStopRPC:    false,
			reusePid:       true,
			installCleanup: true,
			plugin:         &Plugin{Name: "testplugin5", EntryPath: "testplugin5", Revision: "1", Protocol: "unix", Address: addr, Manifest: &Manifest{StartAttempts: 3, StopTimeout: time.Second * 3, PluginInstallationType: acmpb.PluginInstallationType_DYNAMIC_INSTALLATION}, RuntimeInfo: &RuntimeInfo{Pid: -5555}},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			ctx := context.WithValue(context.Background(), client.OverrideConnection, &fakeACS{})
			ts := &testPluginServer{}
			step := &stopStep{cleanup: tc.stopCleanup}
			// Use invalid PID to avoid killing some process unknowingly.
			plugin := tc.plugin
			setupPlugin(ctx, t, plugin, ts)

			if !tc.reusePid {
				tc.psClient.exe = plugin.EntryPath
			}

			setupMockPsClient(t, tc.psClient)

			if err := step.Run(ctx, plugin); err != nil {
				t.Errorf("stopStep.Run(ctx, %+v) failed unexpectedly: %v", plugin, err)
			}

			if plugin.State() != acmpb.CurrentPluginStates_STOPPED {
				t.Errorf("stopStep.Run(ctx, %+v) did not set plugin state to STOPPED", plugin)
			}

			if ts.stopCalled != tc.wantStopRPC {
				t.Errorf("stopStep.Run(ctx, %+v) called stop plugin RPC: %t, want: %t", plugin, ts.stopCalled, tc.wantStopRPC)
			}
			if plugin.RuntimeInfo.Pid != 0 {
				t.Errorf("stopStep.Run(ctx, %+v) did not reset the plugin PID", plugin)
			}
			if plugin.client != nil {
				t.Errorf("stopStep.Run(ctx, %+v) did not reset the plugin client", plugin)
			}

			// Test process was killed.
			wantKilled := (!tc.reusePid && tc.psClient.alive)
			if wantKilled != tc.psClient.procKilled {
				t.Errorf("stopStep.Run(ctx, %+v) killed process: %t, want killed: %t", plugin, tc.psClient.procKilled, wantKilled)
			}

			// Test cleanup happened.
			if exists := file.Exists(plugin.EntryPath, file.TypeFile); exists == tc.installCleanup {
				t.Errorf("stopStep.Run(ctx, %+v) removed plugin file %s: %t, want removed: %t", plugin, plugin.EntryPath, exists, tc.installCleanup)
			}
			if exists := file.Exists(plugin.stateFile(), file.TypeFile); exists == tc.stopCleanup {
				t.Errorf("stopStep.Run(ctx, %+v) removed state file %s: %t, want removed: %t", plugin, plugin.stateFile(), exists, tc.stopCleanup)
			}

			// State directory should not be removed its done only when plugin is
			// explicitly removed.
			if exists := file.Exists(plugin.stateDir(), file.TypeDir); !exists {
				t.Errorf("stopStep.Run(ctx, %+v) removed state dir %s: %t, want removed: %t", plugin, plugin.stateDir(), exists, tc.installCleanup)
			}

			if tc.stopCleanup && ctc.seenName != plugin.FullName() {
				t.Errorf("stopStep.Run(ctx, %+v) did not call remove constraint", plugin)
			}
		})
	}
}

func TestStopStepApi(t *testing.T) {
	wantName := "StopPluginStep"
	wantStatus := acmpb.CurrentPluginStates_STOPPING
	wantErrorStatus := acmpb.CurrentPluginStates_STATE_VALUE_UNSPECIFIED
	step := &stopStep{}

	if step.Name() != wantName {
		t.Errorf("stopStep.Name() = %s, want %s", step.Name(), wantName)
	}
	if step.Status() != wantStatus {
		t.Errorf("stopStep.Status() = %s, want %s", step.Status(), wantStatus)
	}
	if step.ErrorStatus() != wantErrorStatus {
		t.Errorf("stopStep.ErrorStatus() = %s, want %s", step.ErrorStatus(), wantErrorStatus)
	}
}

func TestIsSameExecutablePath(t *testing.T) {
	var validPaths, invalidPaths []string
	var plugin *Plugin
	if runtime.GOOS == "windows" {
		plugin = &Plugin{EntryPath: `C:\Users\testuser\AppData\Local\Temp\testplugin.exe`}
		validPaths = []string{`C:\Users\testuser\AppData\Local\Temp\testplugin.exe.old805949437`, plugin.EntryPath}
		invalidPaths = []string{`C:\Users\testuser\AppData\Local\Temp\testplugin2.exe.old805949437`, `C:\Users\testuser\AppData\Local\Temp\testplugin2.exe`}
	} else {
		plugin = &Plugin{EntryPath: `/var/lib/google/guest_agent/testplugin`}
		validPaths = []string{"/var/lib/google/guest_agent/testplugin (deleted)", plugin.EntryPath}
		invalidPaths = []string{"/var/lib/google/guest_agent/testplugin2 (deleted)", "/var/lib/google/guest_agent/testplugin2"}
	}

	tests := []struct {
		desc         string
		validPaths   []string
		invalidPaths []string
		want         bool
	}{
		{
			desc:       "valid_paths",
			validPaths: validPaths,
			want:       true,
		},
		{
			desc:         "invalid_paths",
			invalidPaths: invalidPaths,
			want:         false,
		},
	}

	for _, tc := range tests {
		t.Run(tc.desc, func(t *testing.T) {
			for _, path := range tc.validPaths {
				if !plugin.isSameExecutablePath(path) {
					t.Errorf("isSameExecutablePath(%q) = false, want true for plugin %+v", path, plugin)
				}
			}
			for _, path := range tc.invalidPaths {
				if plugin.isSameExecutablePath(path) {
					t.Errorf("isSameExecutablePath(%q) = true, want false for plugin %+v", path, plugin)
				}
			}
		})
	}
}
