package mcp

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"cyberstrike-ai/internal/toolguard"

	"go.uber.org/zap"
)

func testToolGuard(t *testing.T, enabled bool) *toolguard.Manager {
	t.Helper()
	guard, err := toolguard.NewManager(toolguard.DefaultConfig())
	if err != nil {
		t.Fatal(err)
	}
	if err := guard.Update(toolguard.Config{Enabled: enabled, Rules: []toolguard.Rule{{
		ID: "government", Name: "政府网站保护", Enabled: true,
		Pattern: `(?i)[a-z0-9.-]+\.gov(?:\.[a-z0-9.-]+)?`,
		Message: "识别到 {match}，禁止攻击政府网站，请检查目标授权。",
	}}}); err != nil {
		t.Fatal(err)
	}
	return guard
}

func assertGuardRefusal(t *testing.T, result *ToolResult, err error) {
	t.Helper()
	message := ToolResultPlainText(result)
	if err != nil {
		t.Fatalf("expected structured refusal, got error: %v", err)
	} else if result == nil || !result.IsError || !result.Blocked {
		t.Fatalf("expected tool error result, got %#v", result)
	}
	for _, text := range []string{toolGuardBlockedPrefix, "禁止攻击政府网站", "agency.gov.cn", "government"} {
		if !strings.Contains(message, text) {
			t.Errorf("refusal %q missing %q", message, text)
		}
	}
}

func TestServerToolGuardBlocksBeforeHandlerAndUpdatesLive(t *testing.T) {
	storage := newInMemoryMonitorStorage()
	server := NewServerWithStorage(zap.NewNop(), storage)
	guard := testToolGuard(t, true)
	server.SetToolGuard(guard)
	var calls, authorized atomic.Int32
	server.SetToolAuthorizer(func(context.Context, string, map[string]interface{}) error {
		authorized.Add(1)
		return nil
	})
	server.RegisterTool(Tool{Name: "scan"}, func(context.Context, map[string]interface{}) (*ToolResult, error) {
		calls.Add(1)
		return &ToolResult{Content: []Content{{Type: "text", Text: "ok"}}}, nil
	})
	args := map[string]interface{}{"command": "scan https://agency.gov.cn"}
	result, executionID, err := server.CallTool(context.Background(), "scan", args)
	assertGuardRefusal(t, result, err)
	if calls.Load() != 0 || authorized.Load() != 1 {
		t.Fatalf("calls=%d authorized=%d, want 0 and 1", calls.Load(), authorized.Load())
	}
	execution, err := storage.GetToolExecution(executionID)
	if err != nil || execution == nil || execution.Status != ToolExecutionStatusBlocked || !strings.Contains(execution.Error, toolGuardBlockedPrefix) {
		t.Fatalf("expected persisted blocked execution, got %#v, err=%v", execution, err)
	}

	result, _, err = server.CallTool(context.Background(), "scan", map[string]interface{}{"target": "example.org"})
	if err != nil || result.IsError || calls.Load() != 1 {
		t.Fatalf("allowed target did not execute: result=%#v calls=%d err=%v", result, calls.Load(), err)
	}
	cfg := guard.Config()
	cfg.Rules[0].Enabled = false
	if err := guard.Update(cfg); err != nil {
		t.Fatal(err)
	}
	result, _, err = server.CallTool(context.Background(), "scan", args)
	if err != nil || result.IsError || calls.Load() != 2 {
		t.Fatalf("disabled rule did not take effect: result=%#v calls=%d err=%v", result, calls.Load(), err)
	}
}

func TestHTTPToolGuardReturnsMCPErrorAndPersistsRefusal(t *testing.T) {
	storage := newInMemoryMonitorStorage()
	server := NewServerWithStorage(zap.NewNop(), storage)
	server.SetToolGuard(testToolGuard(t, true))
	var calls int
	server.RegisterTool(Tool{Name: "scan"}, func(context.Context, map[string]interface{}) (*ToolResult, error) {
		calls++
		return &ToolResult{Content: []Content{{Type: "text", Text: "ok"}}}, nil
	})
	for _, tc := range []struct {
		target  string
		blocked bool
	}{
		{target: "https://agency.gov.cn", blocked: true},
		{target: "https://example.org", blocked: false},
	} {
		body, err := json.Marshal(map[string]interface{}{
			"jsonrpc": "2.0", "id": 1, "method": "tools/call",
			"params": map[string]interface{}{"name": "scan", "arguments": map[string]interface{}{"target": tc.target}},
		})
		if err != nil {
			t.Fatal(err)
		}
		recorder := httptest.NewRecorder()
		server.HandleHTTP(recorder, httptest.NewRequest(http.MethodPost, "/api/mcp", strings.NewReader(string(body))))
		var response Message
		if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
			t.Fatal(err)
		}
		if recorder.Code != http.StatusOK || response.Error != nil {
			t.Fatalf("expected MCP tool result, status=%d body=%s", recorder.Code, recorder.Body)
		}
		var result ToolResult
		if err := json.Unmarshal(response.Result, &result); err != nil {
			t.Fatal(err)
		}
		if tc.blocked {
			assertGuardRefusal(t, &result, nil)
			if calls != 0 {
				t.Fatal("HTTP tool handler ran for a blocked target")
			}
			executions, err := storage.LoadToolExecutions()
			if err != nil || len(executions) != 1 || executions[0].Status != ToolExecutionStatusBlocked || !strings.Contains(executions[0].Error, toolGuardBlockedPrefix) {
				t.Fatalf("expected persisted HTTP refusal, got %#v err=%v", executions, err)
			}
		} else if result.IsError || calls != 1 {
			t.Fatalf("allowed HTTP target did not execute: result=%#v calls=%d", result, calls)
		}
	}
}

func TestExternalToolGuardBlocksBeforeClientAndUpdatesLive(t *testing.T) {
	manager := NewExternalMCPManager(zap.NewNop())
	t.Cleanup(manager.StopAll)
	guard := testToolGuard(t, true)
	manager.SetToolGuard(guard)
	client := newBlockingExternalMCPClient("ok")
	close(client.release)
	manager.mu.Lock()
	manager.clients["lab"] = client
	manager.mu.Unlock()
	args := map[string]interface{}{"target": "https://agency.gov.cn"}
	result, executionID, err := manager.CallTool(context.Background(), "lab::slow_tool", args)
	assertGuardRefusal(t, result, err)
	if client.count.Load() != 0 {
		t.Fatal("external client ran for a blocked target")
	}
	execution, ok := manager.GetExecution(executionID)
	if !ok || execution.Status != ToolExecutionStatusBlocked || !strings.Contains(execution.Error, toolGuardBlockedPrefix) {
		t.Fatalf("expected blocked external execution, got %#v", execution)
	}
	cfg := guard.Config()
	cfg.Enabled = false
	if err := guard.Update(cfg); err != nil {
		t.Fatal(err)
	}
	result, _, err = manager.CallTool(context.Background(), "lab::slow_tool", args)
	if err != nil || result.IsError || client.count.Load() != 1 {
		t.Fatalf("disabled guard did not take effect: result=%#v calls=%d err=%v", result, client.count.Load(), err)
	}
}

func TestExternalToolGuardRechecksQueuedCallsWithoutTrippingCircuit(t *testing.T) {
	manager := NewExternalMCPManager(zap.NewNop())
	t.Cleanup(manager.StopAll)
	manager.toolWaitTimeout = 10 * time.Millisecond
	manager.ConfigureResilience(ExternalMCPResilienceConfig{
		MaxConcurrentPerServer: 1, MaxConcurrentTotal: 4,
		CircuitFailureThreshold: 1, CircuitCooldown: time.Minute,
	})
	guard := testToolGuard(t, false)
	manager.SetToolGuard(guard)
	client := newBlockingExternalMCPClient("ok")
	close(client.release)
	manager.mu.Lock()
	manager.clients["lab"] = client
	manager.mu.Unlock()

	// Occupy the provider slot so the call passes its initial policy check and
	// remains queued until a live rule update is applied.
	release, err := manager.acquireExternalMCPCallSlot(context.Background(), "lab")
	if err != nil {
		t.Fatal(err)
	}
	released := false
	t.Cleanup(func() {
		if !released {
			release()
		}
	})
	_, executionID, err := manager.CallTool(context.Background(), "lab::slow_tool", map[string]interface{}{"target": "agency.gov.cn"})
	if err != nil || executionID == "" {
		t.Fatalf("failed to queue external call: id=%q err=%v", executionID, err)
	}
	deadline := time.After(time.Second)
	ticker := time.NewTicker(time.Millisecond)
	defer ticker.Stop()
	for len(manager.globalSemaphore) != 2 {
		select {
		case <-deadline:
			t.Fatal("execution did not reach the provider slot queue")
		case <-ticker.C:
		}
	}
	cfg := guard.Config()
	cfg.Enabled = true
	if err := guard.Update(cfg); err != nil {
		t.Fatal(err)
	}
	release()
	released = true
	snapshot, err := manager.executionService.Wait(context.Background(), executionID, time.Second)
	if err != nil || snapshot == nil || snapshot.Execution == nil || snapshot.Execution.Status != ToolExecutionStatusBlocked {
		t.Fatalf("expected queued execution to be blocked on policy recheck, got %#v err=%v", snapshot, err)
	}
	assertGuardRefusal(t, snapshot.Execution.Result, nil)
	if client.count.Load() != 0 {
		t.Fatal("queued call bypassed the updated guard")
	}
	manager.mu.RLock()
	runtime := manager.serverRuntimes["lab"]
	failures, openUntil := runtime.consecutiveFailures, runtime.circuitOpenUntil
	manager.mu.RUnlock()
	if failures != 0 || !openUntil.IsZero() {
		t.Fatalf("local policy refusal affected provider circuit: failures=%d openUntil=%v", failures, openUntil)
	}
	result, _, err := manager.CallTool(context.Background(), "lab::slow_tool", map[string]interface{}{"target": "example.org"})
	if err != nil || result.IsError || client.count.Load() != 1 {
		t.Fatalf("allowed call failed after policy refusal: result=%#v calls=%d err=%v", result, client.count.Load(), err)
	}
}
