package connectors

// Permission gating is enforced by middleware.RequirePermission at route registration; not unit-tested here.

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pingcap-inc/nexuscrm-connectors/features/connector/syncstate"
)

// --- fakes ---

type fakeSyncStore struct {
	states []connector_syncstate.SyncState
	err    error
}

func (f *fakeSyncStore) ListByNamespace(_ context.Context, _ string) ([]connector_syncstate.SyncState, error) {
	return f.states, f.err
}

type fakeCredentialStore struct {
	valid bool
	err   error
}

func (f *fakeCredentialStore) CredentialValid(_ context.Context, _ string) (bool, error) {
	return f.valid, f.err
}

// --- helpers ---

func setupHealthRouter(sync ConnectorHealthSyncStore) *gin.Engine {
	return setupHealthRouterWithCredentials(sync, nil)
}

func setupHealthRouterWithCredentials(sync ConnectorHealthSyncStore, creds ConnectorHealthCredentialStore) *gin.Engine {
	gin.SetMode(gin.TestMode)
	r := gin.New()
	h := NewConnectorHealthHandlerWithCredentials(sync, creds)
	h.RegisterRoutes(r.Group("/api/connectors"))
	return r
}

func doHealthRequest(r *gin.Engine, namespace string) *httptest.ResponseRecorder {
	w := httptest.NewRecorder()
	req, _ := http.NewRequest(http.MethodGet, "/api/connectors/"+namespace+"/health", nil)
	r.ServeHTTP(w, req)
	return w
}

// --- Test 1: HappyPath ---

func TestGetHealth_HappyPath(t *testing.T) {
	now := time.Now().UTC()
	state := connector_syncstate.SyncState{
		ID:                "s1",
		InstalledPackId:   "install-1",
		Namespace:         "my-pack",
		Object:            "Lead",
		Direction:         connector_syncstate.DirectionInbound,
		Cursor:            "tok-abc",
		LastSyncAt:        sql.NullTime{Time: now, Valid: true},
		LastErrorMessage:  "",
		LastErrorAt:       sql.NullTime{},
		RunningErrorCount: 0,
		IsActive:          true,
		CreatedDate:       now,
		LastModifiedDate:  now,
	}

	r := setupHealthRouter(&fakeSyncStore{states: []connector_syncstate.SyncState{state}})
	w := doHealthRequest(r, "my-pack")

	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
	}

	var body map[string]interface{}
	if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
		t.Fatalf("invalid JSON: %v", err)
	}
	if body["namespace"] != "my-pack" {
		t.Errorf("namespace: want 'my-pack', got %v", body["namespace"])
	}
	if _, present := body["credentialValid"]; present {
		t.Errorf("credentialValid should be absent when no credential store is configured")
	}
	states, ok := body["states"].([]interface{})
	if !ok {
		t.Fatalf("states is not array: %T", body["states"])
	}
	if len(states) != 1 {
		t.Fatalf("expected 1 state, got %d", len(states))
	}
	s0 := states[0].(map[string]interface{})
	if s0["object"] != "Lead" {
		t.Errorf("object: want 'Lead', got %v", s0["object"])
	}
	if s0["direction"] != connector_syncstate.DirectionInbound {
		t.Errorf("direction: want 'inbound', got %v", s0["direction"])
	}
	if s0["isActive"] != true {
		t.Errorf("isActive: want true, got %v", s0["isActive"])
	}
	// errorCount should be 0 (float64 in JSON)
	if s0["errorCount"] != float64(0) {
		t.Errorf("errorCount: want 0, got %v", s0["errorCount"])
	}
	// lastSyncTime should be non-null since LastSyncAt.Valid=true
	if s0["lastSyncTime"] == nil {
		t.Error("lastSyncTime: expected non-null value")
	}
	// lastErrorMessage must NOT be present in the response: ViewConnectorHealth is a
	// delegatable permission distinct from ManageNamedCredentials; worker error text
	// can contain secrets or tenant data. The DB column is retained worker-internal only.
	if _, present := s0["lastErrorMessage"]; present {
		t.Errorf("lastErrorMessage must not appear in health response")
	}
}

func TestGetHealth_IncludesCredentialValidWhenConfigured(t *testing.T) {
	r := setupHealthRouterWithCredentials(
		&fakeSyncStore{},
		&fakeCredentialStore{valid: true},
	)
	w := doHealthRequest(r, "my-pack")

	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
	}
	var body map[string]interface{}
	if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
		t.Fatalf("invalid JSON: %v", err)
	}
	if body["credentialValid"] != true {
		t.Fatalf("credentialValid = %#v, want true", body["credentialValid"])
	}
}

func TestGetHealth_CredentialValidErrorIsOpaque(t *testing.T) {
	r := setupHealthRouterWithCredentials(
		&fakeSyncStore{},
		&fakeCredentialStore{err: errors.New("secret table missing")},
	)
	w := doHealthRequest(r, "my-pack")

	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
	}
	var body map[string]interface{}
	if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
		t.Fatalf("invalid JSON: %v", err)
	}
	if body["credentialValid"] != false {
		t.Fatalf("credentialValid = %#v, want false", body["credentialValid"])
	}
	if strings.Contains(w.Body.String(), "secret table missing") {
		t.Fatalf("raw credential error leaked")
	}
}

// --- Test 2: InactivePack returns 404 ---

func TestGetHealth_InactivePack(t *testing.T) {
	r := setupHealthRouter(&fakeSyncStore{err: connector_syncstate.ErrConnectorPackInactive})
	w := doHealthRequest(r, "dead-pack")

	if w.Code != http.StatusNotFound {
		t.Fatalf("expected 404, got %d: %s", w.Code, w.Body.String())
	}
	var body map[string]interface{}
	if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
		t.Fatalf("invalid JSON: %v", err)
	}
	if body["error"] != "connector pack not available" {
		t.Errorf("error: want 'connector pack not available', got %v", body["error"])
	}
}

// --- Test 3: EmptyStates returns 200 with empty array (not null) ---

func TestGetHealth_EmptyStates(t *testing.T) {
	r := setupHealthRouter(&fakeSyncStore{states: nil})
	w := doHealthRequest(r, "my-pack")

	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
	}
	var body map[string]interface{}
	if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
		t.Fatalf("invalid JSON: %v", err)
	}
	states, ok := body["states"].([]interface{})
	if !ok {
		t.Fatalf("states is not an array: %T %v", body["states"], body["states"])
	}
	if len(states) != 0 {
		t.Errorf("expected empty states array, got %d elements", len(states))
	}
}

// --- Test 4: Internal error returns opaque 500 (no raw DB/infrastructure details) ---

func TestGetHealth_InternalError_OpaqueResponse(t *testing.T) {
	internalErr := errors.New("internal db reachout failed: table _System_SyncState does not exist")
	r := setupHealthRouter(&fakeSyncStore{err: internalErr})
	w := doHealthRequest(r, "my-pack")

	if w.Code != http.StatusInternalServerError {
		t.Fatalf("expected 500, got %d: %s", w.Code, w.Body.String())
	}
	var body map[string]interface{}
	if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
		t.Fatalf("invalid JSON: %v", err)
	}
	// The response must carry only the opaque envelope; raw error text must not be exposed.
	if body["error"] != "internal server error" {
		t.Errorf("error: want 'internal server error', got %v", body["error"])
	}
	// Ensure raw internal error text is not leaked anywhere in the response body.
	if w.Body.String() == internalErr.Error() {
		t.Errorf("raw internal error text must not appear in response body")
	}
}

// --- Ensure ErrConnectorPackInactive is identifiable via errors.Is ---

func TestErrConnectorPackInactive_IsIdentifiable(t *testing.T) {
	err := connector_syncstate.ErrConnectorPackInactive
	if !errors.Is(err, connector_syncstate.ErrConnectorPackInactive) {
		t.Error("ErrConnectorPackInactive must satisfy errors.Is with itself")
	}
}
