package push

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// === truncateToken tests ===

func TestTruncateToken_LongToken(t *testing.T) {
	token := "abcdefghijklmnopqrstuvwxyz1234567890"
	result := truncateToken(token)
	assert.Equal(t, "abcdefgh...", result)
}

func TestTruncateToken_ShortToken(t *testing.T) {
	token := "abc"
	result := truncateToken(token)
	assert.Equal(t, "abc", result)
}

func TestTruncateToken_ExactlyEightChars(t *testing.T) {
	token := "12345678"
	result := truncateToken(token)
	assert.Equal(t, "12345678", result)
}

func TestTruncateToken_NineChars(t *testing.T) {
	token := "123456789"
	result := truncateToken(token)
	assert.Equal(t, "12345678...", result)
}

func TestTruncateToken_Empty(t *testing.T) {
	result := truncateToken("")
	assert.Equal(t, "", result)
}

// === Client tests ===

func TestClient_SendToIOS_Disabled(t *testing.T) {
	client := &Client{
		enabled:     false,
		apnsBreaker: NewCircuitBreaker("apns"),
		fcmBreaker:  NewCircuitBreaker("fcm"),
	}

	err := client.SendToIOS(context.Background(), []string{"token1"}, "Title", "Body", nil)
	assert.NoError(t, err) // Returns nil when disabled
}

func TestClient_SendToIOS_NilAPNs(t *testing.T) {
	client := &Client{
		enabled:     true,
		apns:        nil, // Not initialized
		apnsBreaker: NewCircuitBreaker("apns"),
		fcmBreaker:  NewCircuitBreaker("fcm"),
	}

	err := client.SendToIOS(context.Background(), []string{"token1"}, "Title", "Body", nil)
	assert.NoError(t, err) // Returns nil when not initialized
}

func TestClient_SendToIOS_CircuitBreakerOpen(t *testing.T) {
	breaker := NewCircuitBreaker("apns", WithFailureThreshold(1))
	breaker.RecordFailure() // Open the circuit

	client := &Client{
		enabled:     true,
		apns:        &APNsClient{}, // Non-nil so we pass the nil check
		apnsBreaker: breaker,
		fcmBreaker:  NewCircuitBreaker("fcm"),
	}

	err := client.SendToIOS(context.Background(), []string{"token1"}, "Title", "Body", nil)
	assert.ErrorIs(t, err, ErrCircuitOpen)
}

func TestClient_SendToAndroid_Disabled(t *testing.T) {
	client := &Client{
		enabled:     false,
		apnsBreaker: NewCircuitBreaker("apns"),
		fcmBreaker:  NewCircuitBreaker("fcm"),
	}

	err := client.SendToAndroid(context.Background(), []string{"token1"}, "Title", "Body", nil)
	assert.NoError(t, err)
}

func TestClient_SendToAndroid_NilFCM(t *testing.T) {
	client := &Client{
		enabled:     true,
		fcm:         nil,
		apnsBreaker: NewCircuitBreaker("apns"),
		fcmBreaker:  NewCircuitBreaker("fcm"),
	}

	err := client.SendToAndroid(context.Background(), []string{"token1"}, "Title", "Body", nil)
	assert.NoError(t, err)
}

func TestClient_SendToAndroid_CircuitBreakerOpen(t *testing.T) {
	breaker := NewCircuitBreaker("fcm", WithFailureThreshold(1))
	breaker.RecordFailure()

	client := &Client{
		enabled:     true,
		fcm:         &FCMClient{}, // Non-nil
		apnsBreaker: NewCircuitBreaker("apns"),
		fcmBreaker:  breaker,
	}

	err := client.SendToAndroid(context.Background(), []string{"token1"}, "Title", "Body", nil)
	assert.ErrorIs(t, err, ErrCircuitOpen)
}

func TestClient_SendToAndroid_Success(t *testing.T) {
	server := serveFCMV1Success(t)
	defer server.Close()

	fcmClient := newTestFCMClient(server.URL)
	client := &Client{
		enabled:     true,
		fcm:         fcmClient,
		apnsBreaker: NewCircuitBreaker("apns"),
		fcmBreaker:  NewCircuitBreaker("fcm"),
	}

	err := client.SendToAndroid(context.Background(), []string{"token1"}, "Title", "Body", nil)
	assert.NoError(t, err)
}

func TestClient_SendToAndroid_Failure_RecordsInBreaker(t *testing.T) {
	server := serveFCMV1Error(t, http.StatusInternalServerError, "INTERNAL", "internal error")
	defer server.Close()

	fcmClient := newTestFCMClient(server.URL)
	breaker := NewCircuitBreaker("fcm", WithFailureThreshold(3))
	client := &Client{
		enabled:     true,
		fcm:         fcmClient,
		apnsBreaker: NewCircuitBreaker("apns"),
		fcmBreaker:  breaker,
	}

	err := client.SendToAndroid(context.Background(), []string{"token1"}, "Title", "Body", nil)
	assert.Error(t, err)
	assert.Equal(t, 1, breaker.Counts())
}

func TestClient_SendToAll_Disabled(t *testing.T) {
	client := &Client{
		enabled:     false,
		apnsBreaker: NewCircuitBreaker("apns"),
		fcmBreaker:  NewCircuitBreaker("fcm"),
	}

	err := client.SendToAll(context.Background(), []string{"ios-token"}, []string{"android-token"}, "Title", "Body", nil)
	assert.NoError(t, err)
}

func TestClient_SendToAll_EmptyTokens(t *testing.T) {
	server := serveFCMV1Success(t)
	defer server.Close()

	fcmClient := newTestFCMClient(server.URL)
	client := &Client{
		enabled:     true,
		fcm:         fcmClient,
		apnsBreaker: NewCircuitBreaker("apns"),
		fcmBreaker:  NewCircuitBreaker("fcm"),
	}

	// No tokens at all — should just return nil
	err := client.SendToAll(context.Background(), []string{}, []string{}, "Title", "Body", nil)
	assert.NoError(t, err)
}

func TestClient_SendToAll_AndroidOnly(t *testing.T) {
	server := serveFCMV1Success(t)
	defer server.Close()

	fcmClient := newTestFCMClient(server.URL)
	client := &Client{
		enabled:     true,
		fcm:         fcmClient,
		apnsBreaker: NewCircuitBreaker("apns"),
		fcmBreaker:  NewCircuitBreaker("fcm"),
	}

	err := client.SendToAll(context.Background(), []string{}, []string{"android-token"}, "Title", "Body", nil)
	assert.NoError(t, err)
}

func TestClient_IsIOSEnabled(t *testing.T) {
	clientWithAPNS := &Client{apns: &APNsClient{}}
	clientWithoutAPNS := &Client{apns: nil}

	assert.True(t, clientWithAPNS.IsIOSEnabled())
	assert.False(t, clientWithoutAPNS.IsIOSEnabled())
}

func TestClient_IsAndroidEnabled(t *testing.T) {
	clientWithFCM := &Client{fcm: &FCMClient{}}
	clientWithoutFCM := &Client{fcm: nil}

	assert.True(t, clientWithFCM.IsAndroidEnabled())
	assert.False(t, clientWithoutFCM.IsAndroidEnabled())
}

func TestClient_HealthCheck(t *testing.T) {
	client := &Client{
		apns: nil,
		fcm:  nil,
	}
	err := client.HealthCheck(context.Background())
	assert.NoError(t, err)

	client.fcm = &FCMClient{}
	err = client.HealthCheck(context.Background())
	assert.NoError(t, err)
}

func TestClient_SendActionableNotification_Disabled(t *testing.T) {
	client := &Client{
		enabled:     false,
		apnsBreaker: NewCircuitBreaker("apns"),
		fcmBreaker:  NewCircuitBreaker("fcm"),
	}

	err := client.SendActionableNotification(context.Background(), []string{"ios"}, []string{"android"}, "Title", "Body", nil, "TASK_DUE")
	assert.NoError(t, err)
}

func TestClient_SendActionableNotification_NilAPNs(t *testing.T) {
	server := serveFCMV1Success(t)
	defer server.Close()

	fcmClient := newTestFCMClient(server.URL)
	client := &Client{
		enabled:     true,
		apns:        nil,
		fcm:         fcmClient,
		apnsBreaker: NewCircuitBreaker("apns"),
		fcmBreaker:  NewCircuitBreaker("fcm"),
	}

	// Should skip iOS and send Android
	err := client.SendActionableNotification(context.Background(), []string{"ios"}, []string{"android"}, "Title", "Body", nil, "TASK_DUE")
	assert.NoError(t, err)
}

func TestClient_SendActionableNotification_APNsBreakerOpen(t *testing.T) {
	server := serveFCMV1Success(t)
	defer server.Close()

	fcmClient := newTestFCMClient(server.URL)
	apnsBreaker := NewCircuitBreaker("apns", WithFailureThreshold(1))
	apnsBreaker.RecordFailure()

	client := &Client{
		enabled:     true,
		apns:        &APNsClient{},
		fcm:         fcmClient,
		apnsBreaker: apnsBreaker,
		fcmBreaker:  NewCircuitBreaker("fcm"),
	}

	err := client.SendActionableNotification(context.Background(), []string{"ios"}, []string{"android"}, "Title", "Body", nil, "TASK_DUE")
	// Should return ErrCircuitOpen because that was the lastErr set
	assert.ErrorIs(t, err, ErrCircuitOpen)
}

// === FCM additional tests ===

func TestFCMV1Send_WithDataPayload(t *testing.T) {
	var receivedData map[string]string

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var req fcmV1Request
		_ = json.NewDecoder(r.Body).Decode(&req)
		receivedData = req.Message.Data

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_ = json.NewEncoder(w).Encode(fcmV1Response{Name: "projects/test/messages/0:12345"})
	}))
	defer server.Close()

	client := newTestFCMClient(server.URL)
	data := map[string]string{
		"task_id":   "42",
		"action":    "complete",
		"deep_link": "/tasks/42",
	}

	err := client.Send(context.Background(), []string{"token"}, "Title", "Body", data)
	require.NoError(t, err)

	assert.Equal(t, "42", receivedData["task_id"])
	assert.Equal(t, "complete", receivedData["action"])
	assert.Equal(t, "/tasks/42", receivedData["deep_link"])
}

func TestFCMV1Send_ContextCancelled(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// The server just hangs — context cancellation should cause the request to fail
		select {}
	}))
	defer server.Close()

	client := newTestFCMClient(server.URL)
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // Cancel immediately

	err := client.Send(ctx, []string{"token"}, "Title", "Body", nil)
	assert.Error(t, err)
}

func TestFCMSendError_ErrorFormatting(t *testing.T) {
	// Short token (no truncation)
	shortErr := &FCMSendError{
		Token:      "abc",
		StatusCode: 500,
		ErrorCode:  FCMErrInternal,
		Message:    "server error",
	}
	assert.Contains(t, shortErr.Error(), "abc")
	assert.Contains(t, shortErr.Error(), "500")
	assert.Contains(t, shortErr.Error(), "INTERNAL")
	assert.Contains(t, shortErr.Error(), "server error")
}

func TestParseFCMV1Error_MalformedJSON(t *testing.T) {
	result := parseFCMV1Error("token123", 500, []byte("not json"))
	assert.Equal(t, 500, result.StatusCode)
	assert.Contains(t, result.Message, "unparseable error response")
}

func TestParseFCMV1Error_ValidJSON(t *testing.T) {
	body := `{"error":{"code":404,"message":"not found","status":"NOT_FOUND"}}`
	result := parseFCMV1Error("token123", 404, []byte(body))
	assert.Equal(t, 404, result.StatusCode)
	assert.Equal(t, FCMErrorCode("NOT_FOUND"), result.ErrorCode)
	assert.Equal(t, "not found", result.Message)
}

// === Platform constants ===

func TestPlatformConstants(t *testing.T) {
	assert.Equal(t, "ios", PlatformIOS)
	assert.Equal(t, "android", PlatformAndroid)
}
