package billing

import (
	"context"
	"errors"
	"net/http"
	"testing"
	"time"

	awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
	"github.com/aws/smithy-go"
	smithyhttp "github.com/aws/smithy-go/transport/http"

	"github.com/mem9-ai/drive9/pkg/meta"
)

func TestSenderDeliversAndAcks(t *testing.T) {
	store := newFakeMetaStore()
	cfg := testConfig()
	cfg.SendEnabled = true
	cfg.SQSQueueURL = "https://sqs.us-west-2.amazonaws.com/123/billing"
	if err := store.WriteBillingUsageWithOutbox(context.Background(), nil, []meta.BillingOutboxRow{
		{CostID: "c1", Payload: []byte(`{"cost":1}`)},
		{CostID: "c2", Payload: []byte(`{"cost":2}`)},
	}); err != nil {
		t.Fatal(err)
	}
	sqsClient := &fakeSQS{}
	s := newSender(cfg, store, sqsClient, "owner-a")
	if err := s.SendOnce(context.Background()); err != nil {
		t.Fatalf("send: %v", err)
	}
	if sqsClient.sentCount() != 2 {
		t.Fatalf("sent = %d, want 2", sqsClient.sentCount())
	}
	if got := store.outboxStatus("c1"); got != meta.BillingOutboxSent {
		t.Fatalf("c1 status = %q, want sent", got)
	}
	// Sent rows are never reclaimed.
	if err := s.SendOnce(context.Background()); err != nil {
		t.Fatal(err)
	}
	if sqsClient.sentCount() != 2 {
		t.Fatalf("re-sent messages: %d", sqsClient.sentCount())
	}
}

func TestSenderShadowModeDoesNotClaim(t *testing.T) {
	store := newFakeMetaStore()
	cfg := testConfig() // SendEnabled=false
	if err := store.WriteBillingUsageWithOutbox(context.Background(), nil, []meta.BillingOutboxRow{
		{CostID: "c1", Payload: []byte(`{}`)},
	}); err != nil {
		t.Fatal(err)
	}
	sqsClient := &fakeSQS{}
	s := newSender(cfg, store, sqsClient, "owner-a")
	if err := s.SendOnce(context.Background()); err != nil {
		t.Fatal(err)
	}
	if sqsClient.sentCount() != 0 {
		t.Fatal("shadow mode must not send")
	}
	if got := store.outboxStatus("c1"); got != meta.BillingOutboxPending {
		t.Fatalf("c1 status = %q, want pending in shadow mode", got)
	}
}

func TestSenderRetryableErrorReturnsToPending(t *testing.T) {
	store := newFakeMetaStore()
	cfg := testConfig()
	cfg.SendEnabled = true
	cfg.SQSQueueURL = "q"
	if err := store.WriteBillingUsageWithOutbox(context.Background(), nil, []meta.BillingOutboxRow{
		{CostID: "c1", Payload: []byte(`{}`)},
	}); err != nil {
		t.Fatal(err)
	}
	s := newSender(cfg, store, &fakeSQS{err: errors.New("connection reset")}, "owner-a")
	if err := s.SendOnce(context.Background()); err != nil {
		t.Fatal(err)
	}
	if got := store.outboxStatus("c1"); got != meta.BillingOutboxPending {
		t.Fatalf("c1 status = %q, want pending after retryable error", got)
	}
}

func TestSenderPermanentErrorAndAttemptExhaustion(t *testing.T) {
	store := newFakeMetaStore()
	cfg := testConfig()
	cfg.SendEnabled = true
	cfg.SQSQueueURL = "q"
	cfg.MaxSendAttempts = 2
	for _, costID := range []string{"c1", "c2"} {
		if err := store.WriteBillingUsageWithOutbox(context.Background(), nil, []meta.BillingOutboxRow{
			{CostID: costID, Payload: []byte(`{}`)},
		}); err != nil {
			t.Fatal(err)
		}
	}

	// c1: non-retryable API error fails permanently on the first attempt.
	apiErr := &smithy.GenericAPIError{Code: "InvalidMessageContents", Message: "bad"}
	s := newSender(cfg, store, &fakeSQS{err: apiErr}, "owner-a")
	if err := s.SendOnce(context.Background()); err != nil {
		t.Fatal(err)
	}
	if got := store.outboxStatus("c1"); got != meta.BillingOutboxPermanentFailed {
		t.Fatalf("c1 status = %q, want permanent_failed", got)
	}
	if got := store.outboxStatus("c2"); got != meta.BillingOutboxPermanentFailed {
		t.Fatalf("c2 status = %q, want permanent_failed (same api error)", got)
	}

	// c3: retryable errors exhaust attempts and then fail permanently.
	if err := store.WriteBillingUsageWithOutbox(context.Background(), nil, []meta.BillingOutboxRow{
		{CostID: "c3", Payload: []byte(`{}`)},
	}); err != nil {
		t.Fatal(err)
	}
	s = newSender(cfg, store, &fakeSQS{err: errors.New("timeout")}, "owner-a")
	if err := s.SendOnce(context.Background()); err != nil {
		t.Fatal(err)
	}
	if got := store.outboxStatus("c3"); got != meta.BillingOutboxPending {
		t.Fatalf("c3 after attempt 1 = %q, want pending", got)
	}
	if err := s.SendOnce(context.Background()); err != nil {
		t.Fatal(err)
	}
	if got := store.outboxStatus("c3"); got != meta.BillingOutboxPermanentFailed {
		t.Fatalf("c3 after attempt 2 = %q, want permanent_failed", got)
	}
}

func TestSenderMisconfiguredFailsFast(t *testing.T) {
	store := newFakeMetaStore()
	cfg := testConfig()
	cfg.SendEnabled = true // but no SQS client / queue URL
	s := newSender(cfg, store, nil, "owner-a")
	if err := s.SendOnce(context.Background()); err == nil {
		t.Fatal("expected fail-fast error when sender is misconfigured")
	}
}

func TestIsRetryableSendError(t *testing.T) {
	if !isRetryableSendError(errors.New("i/o timeout")) {
		t.Fatal("transport errors must be retryable")
	}
	if isRetryableSendError(&smithy.GenericAPIError{Code: "InvalidMessageContents"}) {
		t.Fatal("InvalidMessageContents must be permanent")
	}
	if isRetryableSendError(&smithy.GenericAPIError{Code: "Throttling"}) != true {
		t.Fatal("Throttling must be retryable")
	}
	respErr := &awshttp.ResponseError{ResponseError: &smithyhttp.ResponseError{
		Response: &smithyhttp.Response{Response: &http.Response{StatusCode: http.StatusForbidden}},
	}}
	if isRetryableSendError(respErr) {
		t.Fatal("HTTP 4xx must be permanent")
	}
	respErr.Response.StatusCode = http.StatusBadGateway
	if !isRetryableSendError(respErr) {
		t.Fatal("HTTP 5xx must be retryable")
	}
}

func TestSenderBackoffSchedulesNextAttempt(t *testing.T) {
	store := newFakeMetaStore()
	cfg := testConfig()
	cfg.SendEnabled = true
	cfg.SQSQueueURL = "q"
	if err := store.WriteBillingUsageWithOutbox(context.Background(), nil, []meta.BillingOutboxRow{
		{CostID: "c1", Payload: []byte(`{}`)},
	}); err != nil {
		t.Fatal(err)
	}
	s := newSender(cfg, store, &fakeSQS{err: errors.New("timeout")}, "owner-a")
	if err := s.SendOnce(context.Background()); err != nil {
		t.Fatal(err)
	}
	if got := store.outboxStatus("c1"); got != meta.BillingOutboxPending {
		t.Fatalf("status = %q, want pending", got)
	}
	// The first retry is scheduled 30s out, not immediately.
	if store.nextAttemptAt["c1"].IsZero() || time.Until(store.nextAttemptAt["c1"]) < 25*time.Second {
		t.Fatalf("next_attempt_at = %v, want ~30s backoff", store.nextAttemptAt["c1"])
	}
	if got := backoffForAttempt(7); got != 10*time.Minute {
		t.Fatalf("backoffForAttempt(7) = %v, want capped 10m", got)
	}
}

func TestIsRetryableSendError429(t *testing.T) {
	respErr := &awshttp.ResponseError{ResponseError: &smithyhttp.ResponseError{
		Response: &smithyhttp.Response{Response: &http.Response{StatusCode: http.StatusTooManyRequests}},
	}}
	if !isRetryableSendError(respErr) {
		t.Fatal("HTTP 429 must be retryable")
	}
}
