package jvm

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"GoNavi-Wails/internal/connection"
)

func TestAgentProviderListResourcesBuildsRequestAndDecodesResponse(t *testing.T) {
	provider := NewAgentProvider()
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			t.Fatalf("expected GET request, got %s", r.Method)
		}
		if r.URL.Path != "/gonavi/agent/jvm/resources" {
			t.Fatalf("expected path /gonavi/agent/jvm/resources, got %s", r.URL.Path)
		}
		if got := r.URL.Query().Get("parentPath"); got != "/runtime/cache" {
			t.Fatalf("expected parentPath /runtime/cache, got %q", got)
		}
		if got := r.Header.Get("X-API-Key"); got != "secret-token" {
			t.Fatalf("expected X-API-Key header to pass through, got %q", got)
		}
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode([]ResourceSummary{{
			ID:           "agent.cache",
			Kind:         "folder",
			Name:         "Agent Cache",
			Path:         "/runtime/cache",
			ProviderMode: ModeAgent,
			CanRead:      true,
			CanWrite:     true,
			HasChildren:  true,
		}})
	}))
	defer server.Close()

	items, err := provider.ListResources(context.Background(), newAgentProviderTestConfig(server.URL+"/gonavi/agent/jvm", 3), "/runtime/cache")
	if err != nil {
		t.Fatalf("ListResources returned error: %v", err)
	}
	if len(items) != 1 {
		t.Fatalf("expected 1 resource, got %#v", items)
	}
	if items[0].ProviderMode != ModeAgent || items[0].Path != "/runtime/cache" {
		t.Fatalf("unexpected resource payload: %#v", items[0])
	}
}

func TestAgentProviderGetMonitoringSnapshotDecodesResponse(t *testing.T) {
	provider := &AgentProvider{}
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			t.Fatalf("expected GET request, got %s", r.Method)
		}
		if r.URL.Path != "/gonavi/agent/jvm/metrics" {
			t.Fatalf("expected path /gonavi/agent/jvm/metrics, got %s", r.URL.Path)
		}
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(JVMMonitoringSnapshot{
			Point: JVMMonitoringPoint{
				Timestamp:                  1713945600000,
				ThreadCount:                27,
				HeapUsedBytes:              402653184,
				GCCollectionCount:          128,
				GCDeltaCount:               2,
				ProcessCpuLoad:             0.29,
				CommittedVirtualMemoryBytes: 2147483648,
			},
			RecentGCEvents: []RecentGCEvent{{
				Timestamp:  1713945600000,
				Name:       "ConcurrentMarkSweep",
				DurationMs: 12,
			}},
			AvailableMetrics: []string{"thread.count", "heap.used", "gc.count", "cpu.process", "memory.virtual"},
		})
	}))
	defer server.Close()

	snapshot, err := provider.GetMonitoringSnapshot(context.Background(), newAgentProviderTestConfig(server.URL+"/gonavi/agent/jvm", 3), nil)
	if err != nil {
		t.Fatalf("GetMonitoringSnapshot returned error: %v", err)
	}
	if snapshot.Point.ThreadCount != 27 || snapshot.Point.GCDeltaCount != 2 || snapshot.Point.ProcessCpuLoad != 0.29 {
		t.Fatalf("unexpected monitoring snapshot: %#v", snapshot)
	}
	if len(snapshot.RecentGCEvents) != 1 || snapshot.RecentGCEvents[0].Name != "ConcurrentMarkSweep" {
		t.Fatalf("unexpected recent gc events: %#v", snapshot.RecentGCEvents)
	}
	if len(snapshot.AvailableMetrics) != 5 {
		t.Fatalf("unexpected available metrics: %#v", snapshot)
	}
}

func TestAgentProviderRealAgentRoundTrip(t *testing.T) {
	if _, err := exec.LookPath("java"); err != nil {
		t.Skipf("java 不可用，跳过真实 Agent 集成测试: %v", err)
	}
	if _, err := exec.LookPath("javac"); err != nil {
		t.Skipf("javac 不可用，跳过真实 Agent 集成测试: %v", err)
	}
	if _, err := exec.LookPath("jar"); err != nil {
		t.Skipf("jar 不可用，跳过真实 Agent 集成测试: %v", err)
	}

	provider := NewAgentProvider()
	fixture := startAgentFixture(t)
	cfg := newAgentProviderTestConfig(fixture.baseURL+"/gonavi/agent/jvm", 5)

	waitForTest(t, 10*time.Second, func() error {
		return provider.TestConnection(context.Background(), cfg)
	})

	caps, err := provider.ProbeCapabilities(context.Background(), cfg)
	if err != nil {
		t.Fatalf("ProbeCapabilities returned error: %v", err)
	}
	if len(caps) != 1 || !caps[0].CanBrowse || !caps[0].CanWrite || !caps[0].CanPreview {
		t.Fatalf("unexpected capabilities: %#v", caps)
	}

	root, err := provider.ListResources(context.Background(), cfg, "")
	if err != nil {
		t.Fatalf("ListResources(root) returned error: %v", err)
	}
	if len(root) != 1 || root[0].Name != "Agent Cache" {
		t.Fatalf("unexpected root resources: %#v", root)
	}

	children, err := provider.ListResources(context.Background(), cfg, root[0].Path)
	if err != nil {
		t.Fatalf("ListResources(cache) returned error: %v", err)
	}
	if len(children) != 1 || children[0].Name != "user:1001" {
		t.Fatalf("unexpected child resources: %#v", children)
	}
	entry := children[0]

	before, err := provider.GetValue(context.Background(), cfg, entry.Path)
	if err != nil {
		t.Fatalf("GetValue(before) returned error: %v", err)
	}
	valueMap, ok := before.Value.(map[string]any)
	if !ok {
		t.Fatalf("expected JSON object snapshot, got %#v", before.Value)
	}
	if valueMap["status"] != "cold" {
		t.Fatalf("expected initial status cold, got %#v", before.Value)
	}

	preview, err := provider.PreviewChange(context.Background(), cfg, ChangeRequest{
		ProviderMode:    ModeAgent,
		ResourceID:      entry.Path,
		Action:          "put",
		Reason:          "预热用户缓存",
		ExpectedVersion: before.Version,
		Payload: map[string]any{
			"status": "warm",
			"score":  99,
		},
	})
	if err != nil {
		t.Fatalf("PreviewChange returned error: %v", err)
	}
	if !preview.Allowed || preview.After.ResourceID != entry.Path {
		t.Fatalf("unexpected preview payload: %#v", preview)
	}

	result, err := provider.ApplyChange(context.Background(), cfg, ChangeRequest{
		ProviderMode:    ModeAgent,
		ResourceID:      entry.Path,
		Action:          "put",
		Reason:          "预热用户缓存",
		ExpectedVersion: before.Version,
		Payload: map[string]any{
			"status": "warm",
			"score":  99,
		},
	})
	if err != nil {
		t.Fatalf("ApplyChange returned error: %v", err)
	}
	if result.Status != "applied" {
		t.Fatalf("unexpected apply payload: %#v", result)
	}

	after, err := provider.GetValue(context.Background(), cfg, entry.Path)
	if err != nil {
		t.Fatalf("GetValue(after) returned error: %v", err)
	}
	afterMap, ok := after.Value.(map[string]any)
	if !ok {
		t.Fatalf("expected JSON object snapshot after apply, got %#v", after.Value)
	}
	if afterMap["status"] != "warm" {
		t.Fatalf("expected status warm after apply, got %#v", after.Value)
	}
}

type agentFixtureProcess struct {
	port    int
	baseURL string
	cmd     *exec.Cmd
}

func startAgentFixture(t *testing.T) agentFixtureProcess {
	t.Helper()

	javaBin, err := exec.LookPath("java")
	if err != nil {
		t.Fatalf("look up java failed: %v", err)
	}
	javacBin, err := exec.LookPath("javac")
	if err != nil {
		t.Fatalf("look up javac failed: %v", err)
	}
	jarBin, err := exec.LookPath("jar")
	if err != nil {
		t.Fatalf("look up jar failed: %v", err)
	}

	classesDir := filepath.Join(t.TempDir(), "agent-fixture-classes")
	sourceRoot := filepath.Join(testRepoRoot(t), "internal", "jvm", "testdata", "agentfixture", "src")
	javaFiles, err := filepath.Glob(filepath.Join(sourceRoot, "com", "gonavi", "fixture", "*.java"))
	if err != nil {
		t.Fatalf("glob agent fixture sources failed: %v", err)
	}
	if len(javaFiles) == 0 {
		t.Fatalf("expected agent fixture java files under %s", sourceRoot)
	}

	compileCmd := exec.Command(javacBin, append([]string{"-d", classesDir}, javaFiles...)...)
	output, err := compileCmd.CombinedOutput()
	if err != nil {
		t.Fatalf("compile agent fixture failed: %v\n%s", err, strings.TrimSpace(string(output)))
	}

	manifestPath := filepath.Join(t.TempDir(), "agent-manifest.mf")
	manifest := strings.Join([]string{
		"Premain-Class: com.gonavi.fixture.GoNaviTestAgent",
		"Agent-Class: com.gonavi.fixture.GoNaviTestAgent",
		"Can-Redefine-Classes: false",
		"Can-Retransform-Classes: false",
		"",
	}, "\n")
	if err := os.WriteFile(manifestPath, []byte(manifest), 0o644); err != nil {
		t.Fatalf("write agent manifest failed: %v", err)
	}

	agentJarFile, err := os.CreateTemp("", "gonavi-test-agent-*.jar")
	if err != nil {
		t.Fatalf("create agent jar temp file failed: %v", err)
	}
	agentJar := agentJarFile.Name()
	if closeErr := agentJarFile.Close(); closeErr != nil {
		t.Fatalf("close agent jar temp file failed: %v", closeErr)
	}
	_ = os.Remove(agentJar)
	t.Cleanup(func() {
		_ = os.Remove(agentJar)
	})
	jarCmd := exec.Command(jarBin, "cmf", manifestPath, agentJar, "-C", classesDir, "com")
	output, err = jarCmd.CombinedOutput()
	if err != nil {
		t.Fatalf("package agent jar failed: %v\n%s", err, strings.TrimSpace(string(output)))
	}

	port := reserveTCPPort(t)
	ctx, cancel := context.WithCancel(context.Background())
	t.Cleanup(cancel)

	cmd := exec.CommandContext(
		ctx,
		javaBin,
		fmt.Sprintf("-javaagent:%s=port=%d,token=secret-token", agentJar, port),
		"-cp",
		classesDir,
		"com.gonavi.fixture.AgentHostApp",
	)
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		t.Fatalf("agent fixture stdout pipe failed: %v", err)
	}
	if err := cmd.Start(); err != nil {
		t.Fatalf("start agent fixture failed: %v", err)
	}
	t.Cleanup(func() {
		cancel()
		_ = cmd.Wait()
	})

	ready := make(chan error, 1)
	go func() {
		scanner := bufio.NewScanner(stdout)
		for scanner.Scan() {
			if strings.TrimSpace(scanner.Text()) == "AGENT_READY" {
				ready <- nil
				return
			}
		}
		if err := scanner.Err(); err != nil {
			ready <- fmt.Errorf("agent fixture readiness read failed: %w", err)
			return
		}
		ready <- fmt.Errorf("agent fixture terminated before readiness signal")
	}()

	select {
	case err := <-ready:
		if err != nil {
			t.Fatalf("wait agent fixture ready failed: %v", err)
		}
	case <-time.After(20 * time.Second):
		t.Fatal("agent fixture did not become ready within 20s")
	}

	waitForTest(t, 10*time.Second, func() error {
		conn, dialErr := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 500*time.Millisecond)
		if dialErr != nil {
			return dialErr
		}
		_ = conn.Close()
		return nil
	})

	return agentFixtureProcess{
		port:    port,
		baseURL: fmt.Sprintf("http://127.0.0.1:%d", port),
		cmd:     cmd,
	}
}

func newAgentProviderTestConfig(baseURL string, timeoutSeconds int) connection.ConnectionConfig {
	readOnly := false
	return connection.ConnectionConfig{
		Type:    "jvm",
		Timeout: timeoutSeconds,
		JVM: connection.JVMConfig{
			ReadOnly:      &readOnly,
			AllowedModes:  []string{ModeAgent},
			PreferredMode: ModeAgent,
			Agent: connection.JVMAgentConfig{
				BaseURL:        baseURL,
				APIKey:         "secret-token",
				TimeoutSeconds: timeoutSeconds,
			},
		},
	}
}
