package jvm

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

	"GoNavi-Wails/internal/connection"
)

func TestHTTPProviderListResourcesBuildsRequestAndDecodesResponse(t *testing.T) {
	provider := NewHTTPProvider()
	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 != "/manage/jvm/resources" {
			t.Fatalf("expected path /manage/jvm/resources, got %s", r.URL.Path)
		}
		if got := r.URL.Query().Get("parentPath"); got != "/cache/orders" {
			t.Fatalf("expected parentPath /cache/orders, 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:           "cache.orders",
			Kind:         "folder",
			Name:         "Orders",
			Path:         "/cache/orders",
			ProviderMode: ModeEndpoint,
			CanRead:      true,
			CanWrite:     true,
			HasChildren:  true,
		}})
	}))
	defer server.Close()

	items, err := provider.ListResources(context.Background(), newHTTPProviderTestConfig(server.URL+"/manage/jvm/", 3), "/cache/orders")
	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 != ModeEndpoint || items[0].Path != "/cache/orders" {
		t.Fatalf("unexpected resource payload: %#v", items[0])
	}
}

func TestHTTPProviderGetValueDecodesResponse(t *testing.T) {
	provider := NewHTTPProvider()
	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 != "/runtime/value" {
			t.Fatalf("expected path /runtime/value, got %s", r.URL.Path)
		}
		if got := r.URL.Query().Get("resourcePath"); got != "/cache/orders" {
			t.Fatalf("expected resourcePath /cache/orders, got %q", got)
		}
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(ValueSnapshot{
			ResourceID: "/cache/orders",
			Kind:       "entry",
			Format:     "json",
			Version:    "v1",
			Value: map[string]any{
				"status": "ready",
			},
		})
	}))
	defer server.Close()

	value, err := provider.GetValue(context.Background(), newHTTPProviderTestConfig(server.URL+"/runtime", 3), "/cache/orders")
	if err != nil {
		t.Fatalf("GetValue returned error: %v", err)
	}
	if value.ResourceID != "/cache/orders" || value.Version != "v1" {
		t.Fatalf("unexpected value payload: %#v", value)
	}
}

func TestHTTPProviderGetMonitoringSnapshotDecodesResponse(t *testing.T) {
	provider := &HTTPProvider{}
	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 != "/manage/jvm/metrics" {
			t.Fatalf("expected path /manage/jvm/metrics, got %s", r.URL.Path)
		}
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(JVMMonitoringSnapshot{
			Point: JVMMonitoringPoint{
				Timestamp:        1713945600000,
				ThreadCount:      18,
				HeapUsedBytes:    805306368,
				ProcessCpuLoad:   0.48,
				ProcessRssBytes:  1879048192,
				LoadedClassCount: 4096,
			},
			RecentGCEvents: []RecentGCEvent{{
				Timestamp:  1713945600000,
				Name:       "G1 Old Generation",
				DurationMs: 41,
			}},
			AvailableMetrics: []string{"thread.count", "heap.used", "cpu.process", "memory.rss", "class.loading"},
			MissingMetrics:   []string{"cpu.system"},
			ProviderWarnings: []string{"endpoint cpu metric unavailable"},
		})
	}))
	defer server.Close()

	snapshot, err := provider.GetMonitoringSnapshot(context.Background(), newHTTPProviderTestConfig(server.URL+"/manage/jvm", 3), nil)
	if err != nil {
		t.Fatalf("GetMonitoringSnapshot returned error: %v", err)
	}
	if snapshot.Point.ThreadCount != 18 || snapshot.Point.HeapUsedBytes != 805306368 || snapshot.Point.ProcessRssBytes != 1879048192 {
		t.Fatalf("unexpected monitoring snapshot: %#v", snapshot)
	}
	if len(snapshot.RecentGCEvents) != 1 || snapshot.RecentGCEvents[0].Name != "G1 Old Generation" {
		t.Fatalf("unexpected recent gc events: %#v", snapshot.RecentGCEvents)
	}
	if len(snapshot.MissingMetrics) != 1 || snapshot.MissingMetrics[0] != "cpu.system" {
		t.Fatalf("unexpected missing metrics: %#v", snapshot)
	}
}

func TestHTTPProviderPreviewChangeAndApplySendJSONBody(t *testing.T) {
	provider := NewHTTPProvider()
	request := ChangeRequest{
		ProviderMode:    ModeEndpoint,
		ResourceID:      "/cache/orders",
		Action:          "put",
		Reason:          "refresh cache",
		ExpectedVersion: "v1",
		Payload: map[string]any{
			"status": "warm",
		},
	}

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		body, err := io.ReadAll(r.Body)
		if err != nil {
			t.Fatalf("failed to read request body: %v", err)
		}
		defer r.Body.Close()
		if contentType := r.Header.Get("Content-Type"); !strings.Contains(contentType, "application/json") {
			t.Fatalf("expected JSON content type, got %q", contentType)
		}

		var got ChangeRequest
		if err := json.Unmarshal(body, &got); err != nil {
			t.Fatalf("failed to decode request body: %v", err)
		}
		if got.ResourceID != request.ResourceID || got.Action != request.Action || got.ExpectedVersion != request.ExpectedVersion {
			t.Fatalf("unexpected request body: %#v", got)
		}

		w.Header().Set("Content-Type", "application/json")
		switch r.URL.Path {
		case "/manage/jvm/preview":
			if r.Method != http.MethodPost {
				t.Fatalf("expected POST /preview, got %s", r.Method)
			}
			_ = json.NewEncoder(w).Encode(ChangePreview{
				Allowed:   true,
				Summary:   "preview ready",
				RiskLevel: "low",
				Before: ValueSnapshot{
					ResourceID: request.ResourceID,
					Kind:       "entry",
					Format:     "json",
				},
				After: ValueSnapshot{
					ResourceID: request.ResourceID,
					Kind:       "entry",
					Format:     "json",
					Value: map[string]any{
						"status": "warm",
					},
				},
			})
		case "/manage/jvm/apply":
			if r.Method != http.MethodPost {
				t.Fatalf("expected POST /apply, got %s", r.Method)
			}
			_ = json.NewEncoder(w).Encode(ApplyResult{
				Status:  "applied",
				Message: "updated",
				UpdatedValue: ValueSnapshot{
					ResourceID: request.ResourceID,
					Kind:       "entry",
					Format:     "json",
					Value: map[string]any{
						"status": "warm",
					},
				},
			})
		default:
			t.Fatalf("unexpected request path: %s", r.URL.Path)
		}
	}))
	defer server.Close()

	preview, err := provider.PreviewChange(context.Background(), newHTTPProviderTestConfig(server.URL+"/manage/jvm", 3), request)
	if err != nil {
		t.Fatalf("PreviewChange returned error: %v", err)
	}
	if !preview.Allowed || preview.Summary != "preview ready" {
		t.Fatalf("unexpected preview payload: %#v", preview)
	}

	result, err := provider.ApplyChange(context.Background(), newHTTPProviderTestConfig(server.URL+"/manage/jvm", 3), request)
	if err != nil {
		t.Fatalf("ApplyChange returned error: %v", err)
	}
	if result.Status != "applied" || result.UpdatedValue.ResourceID != request.ResourceID {
		t.Fatalf("unexpected apply payload: %#v", result)
	}
}

func TestHTTPProviderProbeCapabilitiesReflectsReadOnlyConnection(t *testing.T) {
	provider := NewHTTPProvider()
	cfg := newHTTPProviderTestConfig("https://orders.internal/manage/jvm", 3)
	readOnly := true
	cfg.JVM.ReadOnly = &readOnly

	caps, err := provider.ProbeCapabilities(context.Background(), cfg)
	if err != nil {
		t.Fatalf("ProbeCapabilities returned error: %v", err)
	}
	if len(caps) != 1 {
		t.Fatalf("expected one capability, got %#v", caps)
	}
	if caps[0].CanWrite {
		t.Fatalf("expected endpoint capability to be readonly, got %#v", caps[0])
	}
	if caps[0].Reason != "当前连接只读" {
		t.Fatalf("expected readonly reason, got %#v", caps[0])
	}
}

func TestHTTPProviderProbeCapabilitiesReturnsConfigValidationError(t *testing.T) {
	provider := NewHTTPProvider()

	_, err := provider.ProbeCapabilities(context.Background(), connection.ConnectionConfig{
		Type: "jvm",
		JVM: connection.JVMConfig{
			Endpoint: connection.JVMEndpointConfig{
				BaseURL: "",
			},
		},
	})
	if err == nil {
		t.Fatal("expected endpoint config validation error")
	}
	if !strings.Contains(err.Error(), "endpoint baseURL is required") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestHTTPProviderReturnsReadableStatusErrors(t *testing.T) {
	provider := NewHTTPProvider()

	tests := []struct {
		name string
		path string
		code int
		body string
		call func(context.Context, Provider, connection.ConnectionConfig) error
		want []string
	}{
		{
			name: "list resources unauthorized",
			path: "/resources",
			code: http.StatusUnauthorized,
			body: "missing api key",
			call: func(ctx context.Context, provider Provider, cfg connection.ConnectionConfig) error {
				_, err := provider.ListResources(ctx, cfg, "/cache/orders")
				return err
			},
			want: []string{"list resources", "401 Unauthorized", "missing api key"},
		},
		{
			name: "get value forbidden",
			path: "/value",
			code: http.StatusForbidden,
			body: "access denied",
			call: func(ctx context.Context, provider Provider, cfg connection.ConnectionConfig) error {
				_, err := provider.GetValue(ctx, cfg, "/cache/orders")
				return err
			},
			want: []string{"get value", "403 Forbidden", "access denied"},
		},
		{
			name: "preview change server error",
			path: "/preview",
			code: http.StatusInternalServerError,
			body: "preview backend exploded",
			call: func(ctx context.Context, provider Provider, cfg connection.ConnectionConfig) error {
				_, err := provider.PreviewChange(ctx, cfg, ChangeRequest{
					ProviderMode: ModeEndpoint,
					ResourceID:   "/cache/orders",
					Action:       "put",
					Reason:       "refresh cache",
				})
				return err
			},
			want: []string{"preview change", "500 Internal Server Error", "preview backend exploded"},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				if r.URL.Path != "/manage/jvm"+tc.path {
					t.Fatalf("expected path %s, got %s", "/manage/jvm"+tc.path, r.URL.Path)
				}
				http.Error(w, tc.body, tc.code)
			}))
			defer server.Close()

			err := tc.call(context.Background(), provider, newHTTPProviderTestConfig(server.URL+"/manage/jvm", 3))
			if err == nil {
				t.Fatal("expected request error")
			}
			for _, fragment := range tc.want {
				if !strings.Contains(err.Error(), fragment) {
					t.Fatalf("expected error %q to contain %q", err.Error(), fragment)
				}
			}
		})
	}
}

func TestHTTPProviderReturnsInvalidJSONError(t *testing.T) {
	provider := NewHTTPProvider()
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"resourceId":`))
	}))
	defer server.Close()

	_, err := provider.GetValue(context.Background(), newHTTPProviderTestConfig(server.URL, 3), "/cache/orders")
	if err == nil {
		t.Fatal("expected invalid JSON error")
	}
	if !strings.Contains(strings.ToLower(err.Error()), "invalid json") {
		t.Fatalf("expected invalid JSON error, got %v", err)
	}
}

func TestHTTPProviderReturnsTimeoutError(t *testing.T) {
	provider := NewHTTPProvider()
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(1200 * time.Millisecond)
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode([]ResourceSummary{})
	}))
	defer server.Close()

	_, err := provider.ListResources(context.Background(), newHTTPProviderTestConfig(server.URL, 1), "/cache/orders")
	if err == nil {
		t.Fatal("expected timeout error")
	}
	if !strings.Contains(err.Error(), "timed out after 1s") {
		t.Fatalf("expected timeout error, got %v", err)
	}
}

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

	provider := NewHTTPProvider()
	fixture := startEndpointFixture(t)
	cfg := newHTTPProviderTestConfig(fixture.baseURL+"/manage/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)
	}

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

	children, err := provider.ListResources(context.Background(), cfg, rootItems[0].Path)
	if err != nil {
		t.Fatalf("ListResources(child) returned error: %v", err)
	}
	stateResource := findResourceByName(t, children, "State")

	before, err := provider.GetValue(context.Background(), cfg, stateResource.Path)
	if err != nil {
		t.Fatalf("GetValue(before) returned error: %v", err)
	}
	beforeMap, ok := before.Value.(map[string]any)
	if !ok || beforeMap["status"] != "warm" || strings.TrimSpace(before.Version) == "" {
		t.Fatalf("unexpected initial value snapshot: %#v", before)
	}

	preview, err := provider.PreviewChange(context.Background(), cfg, ChangeRequest{
		ProviderMode:    ModeEndpoint,
		ResourceID:      stateResource.Path,
		Action:          "put",
		Reason:          "更新订单缓存状态",
		ExpectedVersion: before.Version,
		Payload: map[string]any{
			"status":      "hot",
			"lastUpdated": "preview-check",
		},
	})
	if err != nil {
		t.Fatalf("PreviewChange returned error: %v", err)
	}
	previewAfter, ok := preview.After.Value.(map[string]any)
	if !preview.Allowed || !ok || previewAfter["status"] != "hot" {
		t.Fatalf("unexpected preview payload: %#v", preview)
	}

	result, err := provider.ApplyChange(context.Background(), cfg, ChangeRequest{
		ProviderMode:    ModeEndpoint,
		ResourceID:      stateResource.Path,
		Action:          "put",
		Reason:          "更新订单缓存状态",
		ExpectedVersion: before.Version,
		Payload: map[string]any{
			"status":      "hot",
			"lastUpdated": "manual-check",
		},
	})
	if err != nil {
		t.Fatalf("ApplyChange returned error: %v", err)
	}
	updatedMap, ok := result.UpdatedValue.Value.(map[string]any)
	if result.Status != "applied" || !ok || updatedMap["status"] != "hot" || updatedMap["lastUpdated"] != "manual-check" {
		t.Fatalf("unexpected apply result: %#v", result)
	}

	after, err := provider.GetValue(context.Background(), cfg, stateResource.Path)
	if err != nil {
		t.Fatalf("GetValue(after) returned error: %v", err)
	}
	afterMap, ok := after.Value.(map[string]any)
	if !ok || afterMap["status"] != "hot" || after.Version == before.Version {
		t.Fatalf("unexpected updated value snapshot: %#v", after)
	}
}

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

func startEndpointFixture(t *testing.T) endpointFixtureProcess {
	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)
	}

	classesDir := filepath.Join(t.TempDir(), "endpoint-fixture-classes")
	sourceRoot := filepath.Join(testRepoRoot(t), "internal", "jvm", "testdata", "endpointfixture", "src")
	javaFiles, err := filepath.Glob(filepath.Join(sourceRoot, "com", "gonavi", "fixture", "*.java"))
	if err != nil {
		t.Fatalf("glob endpoint fixture sources failed: %v", err)
	}
	if len(javaFiles) == 0 {
		t.Fatalf("expected endpoint 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 endpoint fixture 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, "-cp", classesDir, "com.gonavi.fixture.EndpointTestServer", fmt.Sprintf("%d", port))
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		t.Fatalf("endpoint fixture stdout pipe failed: %v", err)
	}
	if err := cmd.Start(); err != nil {
		t.Fatalf("start endpoint fixture failed: %v", err)
	}
	t.Cleanup(func() {
		cancel()
		_ = cmd.Wait()
	})

	ready := make(chan error, 1)
	go func() {
		line, readErr := bufio.NewReader(stdout).ReadString('\n')
		if readErr != nil {
			ready <- fmt.Errorf("endpoint fixture readiness read failed: %w", readErr)
			return
		}
		if strings.TrimSpace(line) != "READY" {
			ready <- fmt.Errorf("unexpected endpoint fixture readiness line: %q", strings.TrimSpace(line))
			return
		}
		ready <- nil
	}()

	select {
	case err := <-ready:
		if err != nil {
			t.Fatalf("wait endpoint fixture ready failed: %v", err)
		}
	case <-time.After(20 * time.Second):
		t.Fatal("endpoint 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 endpointFixtureProcess{
		port:    port,
		baseURL: fmt.Sprintf("http://127.0.0.1:%d", port),
		cmd:     cmd,
	}
}

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