package cart

import (
	"reflect"
	"slices"
	"strings"
	"testing"
	"time"

	"github.com/gogo/protobuf/proto"
	anypb "google.golang.org/protobuf/types/known/anypb"

	"git.tornberg.me/go-cart-actor/pkg/actor"
	messages "git.tornberg.me/go-cart-actor/pkg/messages"
)

// ----------------------
// Helper constructors
// ----------------------

func newTestGrain() *CartGrain {
	return NewCartGrain(123, time.Now())
}

func newRegistry() actor.MutationRegistry {
	return NewCartMultationRegistry()
}

func msgAddItem(sku string, price int64, qty int32, storePtr *string) *messages.AddItem {
	return &messages.AddItem{
		Sku:      sku,
		Price:    price,
		Quantity: qty,
		// Tax left 0 -> handler uses default 25%
		StoreId: storePtr,
	}
}

func msgChangeQty(id uint32, qty int32) *messages.ChangeQuantity {
	return &messages.ChangeQuantity{Id: id, Quantity: qty}
}

func msgRemoveItem(id uint32) *messages.RemoveItem {
	return &messages.RemoveItem{Id: id}
}

func msgSetDelivery(provider string, items ...uint32) *messages.SetDelivery {
	uitems := make([]uint32, len(items))
	copy(uitems, items)
	return &messages.SetDelivery{Provider: provider, Items: uitems}
}

func msgSetPickupPoint(deliveryId uint32, id string) *messages.SetPickupPoint {
	return &messages.SetPickupPoint{
		DeliveryId: deliveryId,
		Id:         id,
		Name:       ptr("Pickup"),
		Address:    ptr("Street 1"),
		City:       ptr("Town"),
		Zip:        ptr("12345"),
		Country:    ptr("SE"),
	}
}

func msgClearCart() *messages.ClearCartRequest {
	return &messages.ClearCartRequest{}
}

func msgAddVoucher(code string, value int64, rules ...*messages.VoucherRule) *messages.AddVoucher {
	return &messages.AddVoucher{Code: code, Value: value, VoucherRules: rules}
}

func msgRemoveVoucher(id uint32) *messages.RemoveVoucher {
	return &messages.RemoveVoucher{Id: id}
}

func msgInitializeCheckout(orderId, status string, inProgress bool) *messages.InitializeCheckout {
	return &messages.InitializeCheckout{OrderId: orderId, Status: status, PaymentInProgress: inProgress}
}

func msgOrderCreated(orderId, status string) *messages.OrderCreated {
	return &messages.OrderCreated{OrderId: orderId, Status: status}
}

func ptr[T any](v T) *T { return &v }

// ----------------------
// Apply helpers
// ----------------------

func applyOne(t *testing.T, reg actor.MutationRegistry, g *CartGrain, msg proto.Message) actor.ApplyResult {
	t.Helper()
	results, err := reg.Apply(g, msg)
	if err != nil {
		t.Fatalf("unexpected registry-level error applying %T: %v", msg, err)
	}
	if len(results) != 1 {
		t.Fatalf("expected exactly one ApplyResult, got %d", len(results))
	}
	return results[0]
}

// Expect success (nil error inside ApplyResult).
func applyOK(t *testing.T, reg actor.MutationRegistry, g *CartGrain, msg proto.Message) {
	t.Helper()
	res := applyOne(t, reg, g, msg)
	if res.Error != nil {
		t.Fatalf("expected mutation %s (%T) to succeed, got error: %v", res.Type, msg, res.Error)
	}
}

// Expect an error matching substring.
func applyErrorContains(t *testing.T, reg actor.MutationRegistry, g *CartGrain, msg proto.Message, substr string) {
	t.Helper()
	res := applyOne(t, reg, g, msg)
	if res.Error == nil {
		t.Fatalf("expected error applying %T, got nil", msg)
	}
	if substr != "" && !strings.Contains(res.Error.Error(), substr) {
		t.Fatalf("error mismatch, want substring %q got %q", substr, res.Error.Error())
	}
}

// ----------------------
// Tests
// ----------------------

func TestMutationRegistryCoverage(t *testing.T) {
	reg := newRegistry()

	expected := []string{
		"AddItem",
		"ChangeQuantity",
		"RemoveItem",
		"InitializeCheckout",
		"OrderCreated",
		"RemoveDelivery",
		"SetDelivery",
		"SetPickupPoint",
		"ClearCartRequest",
		"AddVoucher",
		"RemoveVoucher",
		"UpsertSubscriptionDetails",
	}

	names := reg.(*actor.ProtoMutationRegistry).RegisteredMutations()
	for _, want := range expected {
		if !slices.Contains(names, want) {
			t.Fatalf("registry missing mutation %s; got %v", want, names)
		}
	}

	// Create() by name returns correct concrete type.
	for _, name := range expected {
		msg, ok := reg.Create(name)
		if !ok {
			t.Fatalf("Create failed for %s", name)
		}
		rt := reflect.TypeOf(msg)
		if rt.Kind() == reflect.Ptr {
			rt = rt.Elem()
		}
		if rt.Name() != name {
			t.Fatalf("Create(%s) returned wrong type %s", name, rt.Name())
		}
	}

	// Unregistered create
	if m, ok := reg.Create("DoesNotExist"); ok || m != nil {
		t.Fatalf("Create should fail for unknown; got (%T,%v)", m, ok)
	}

	// GetTypeName sanity
	add := &messages.AddItem{}
	nm, ok := reg.GetTypeName(add)
	if !ok || nm != "AddItem" {
		t.Fatalf("GetTypeName failed for AddItem, got (%q,%v)", nm, ok)
	}

	// Apply unregistered message -> result should contain ErrMutationNotRegistered, no top-level error
	results, err := reg.Apply(newTestGrain(), &messages.Noop{})
	if err != nil {
		t.Fatalf("unexpected top-level error applying unregistered mutation: %v", err)
	}
	if len(results) != 1 || results[0].Error == nil || results[0].Error != actor.ErrMutationNotRegistered {
		t.Fatalf("expected ApplyResult with ErrMutationNotRegistered, got %#v", results)
	}
}

func TestAddItemAndMerging(t *testing.T) {
	reg := newRegistry()
	g := newTestGrain()

	// Merge scenario (same SKU + same store pointer)
	add1 := msgAddItem("SKU-1", 1000, 2, nil)
	applyOK(t, reg, g, add1)

	if len(g.Items) != 1 || g.Items[0].Quantity != 2 {
		t.Fatalf("expected first item added; items=%d qty=%d", len(g.Items), g.Items[0].Quantity)
	}

	applyOK(t, reg, g, msgAddItem("SKU-1", 1000, 3, nil)) // should merge
	if len(g.Items) != 1 || g.Items[0].Quantity != 5 {
		t.Fatalf("expected merge quantity=5 items=%d qty=%d", len(g.Items), g.Items[0].Quantity)
	}

	// Different store pointer -> new line
	store := "S1"
	applyOK(t, reg, g, msgAddItem("SKU-1", 1000, 1, &store))
	if len(g.Items) != 2 {
		t.Fatalf("expected second line for different store pointer; items=%d", len(g.Items))
	}

	// Same store pointer & SKU -> merge with second line
	applyOK(t, reg, g, msgAddItem("SKU-1", 1000, 4, &store))
	if len(g.Items) != 2 || g.Items[1].Quantity != 5 {
		t.Fatalf("expected merge on second line; items=%d second.qty=%d", len(g.Items), g.Items[1].Quantity)
	}

	// Invalid quantity
	applyErrorContains(t, reg, g, msgAddItem("BAD", 1000, 0, nil), "invalid quantity")
}

func TestChangeQuantityBehavior(t *testing.T) {
	reg := newRegistry()
	g := newTestGrain()

	applyOK(t, reg, g, msgAddItem("A", 1500, 2, nil))
	id := g.Items[0].Id

	// Increase quantity
	applyOK(t, reg, g, msgChangeQty(id, 5))
	if g.Items[0].Quantity != 5 {
		t.Fatalf("quantity not updated expected=5 got=%d", g.Items[0].Quantity)
	}

	// Remove item by setting <=0
	applyOK(t, reg, g, msgChangeQty(id, 0))
	if len(g.Items) != 0 {
		t.Fatalf("expected item removed; items=%d", len(g.Items))
	}

	// Not found
	applyErrorContains(t, reg, g, msgChangeQty(9999, 1), "not found")
}

func TestRemoveItemBehavior(t *testing.T) {
	reg := newRegistry()
	g := newTestGrain()

	applyOK(t, reg, g, msgAddItem("X", 1200, 1, nil))
	id := g.Items[0].Id

	applyOK(t, reg, g, msgRemoveItem(id))
	if len(g.Items) != 0 {
		t.Fatalf("expected item removed; items=%d", len(g.Items))
	}

	applyErrorContains(t, reg, g, msgRemoveItem(id), "not found")
}

func TestDeliveryMutations(t *testing.T) {
	reg := newRegistry()
	g := newTestGrain()

	applyOK(t, reg, g, msgAddItem("D1", 1000, 1, nil))
	applyOK(t, reg, g, msgAddItem("D2", 2000, 1, nil))
	i1 := g.Items[0].Id

	// Explicit items
	applyOK(t, reg, g, msgSetDelivery("POSTNORD", i1))
	if len(g.Deliveries) != 1 || len(g.Deliveries[0].Items) != 1 || g.Deliveries[0].Items[0] != i1 {
		t.Fatalf("delivery not created as expected: %+v", g.Deliveries)
	}

	// Attempt to attach an already-delivered item
	applyErrorContains(t, reg, g, msgSetDelivery("POSTNORD", i1), "already has a delivery")

	// Attach remaining item via empty list (auto include items without delivery)
	applyOK(t, reg, g, msgSetDelivery("DHL"))
	if len(g.Deliveries) != 2 {
		t.Fatalf("expected second delivery; deliveries=%d", len(g.Deliveries))
	}

	// Non-existent item
	applyErrorContains(t, reg, g, msgSetDelivery("UPS", 99999), "not found")

	// No eligible items left
	applyErrorContains(t, reg, g, msgSetDelivery("UPS"), "no eligible items")

	// Set pickup point on first delivery
	did := g.Deliveries[0].Id
	applyOK(t, reg, g, msgSetPickupPoint(did, "PP1"))
	if g.Deliveries[0].PickupPoint == nil || g.Deliveries[0].PickupPoint.Id != "PP1" {
		t.Fatalf("pickup point not set correctly: %+v", g.Deliveries[0].PickupPoint)
	}

	// Bad delivery id
	applyErrorContains(t, reg, g, msgSetPickupPoint(9999, "PPX"), "delivery id")

	// Remove delivery
	applyOK(t, reg, g, &messages.RemoveDelivery{Id: did})
	if len(g.Deliveries) != 1 || g.Deliveries[0].Id == did {
		t.Fatalf("expected first delivery removed, remaining: %+v", g.Deliveries)
	}

	// Remove delivery not found
	applyErrorContains(t, reg, g, &messages.RemoveDelivery{Id: did}, "not found")
}

func TestClearCart(t *testing.T) {
	reg := newRegistry()
	g := newTestGrain()

	applyOK(t, reg, g, msgAddItem("X", 1000, 2, nil))
	applyOK(t, reg, g, msgSetDelivery("P", g.Items[0].Id))

	applyOK(t, reg, g, msgClearCart())

	if len(g.Items) != 0 || len(g.Deliveries) != 0 {
		t.Fatalf("expected cart cleared; items=%d deliveries=%d", len(g.Items), len(g.Deliveries))
	}
}

func TestVoucherMutations(t *testing.T) {
	reg := newRegistry()
	g := newTestGrain()

	applyOK(t, reg, g, msgAddItem("VOUCH", 10000, 1, nil))
	applyOK(t, reg, g, msgAddVoucher("PROMO", 5000))

	if len(g.Vouchers) != 1 {
		t.Fatalf("voucher not stored")
	}
	if g.TotalDiscount.IncVat != 5000 {
		t.Fatalf("expected discount 5000 got %d", g.TotalDiscount.IncVat)
	}
	if g.TotalPrice.IncVat != 5000 {
		t.Fatalf("expected total price 5000 got %d", g.TotalPrice.IncVat)
	}

	// Duplicate voucher code
	applyErrorContains(t, reg, g, msgAddVoucher("PROMO", 1000), "already applied")

	// Add a large voucher (should not apply because value > total price)
	applyOK(t, reg, g, msgAddVoucher("BIG", 100000))
	if len(g.Vouchers) != 2 {
		t.Fatalf("expected second voucher stored")
	}
	if g.TotalDiscount.IncVat != 5000 || g.TotalPrice.IncVat != 5000 {
		t.Fatalf("large voucher incorrectly applied discount=%d total=%d",
			g.TotalDiscount.IncVat, g.TotalPrice.IncVat)
	}

	// Remove existing voucher
	firstId := g.Vouchers[0].Id
	applyOK(t, reg, g, msgRemoveVoucher(firstId))

	if slices.ContainsFunc(g.Vouchers, func(v *Voucher) bool { return v.Id == firstId }) {
		t.Fatalf("voucher id %d not removed", firstId)
	}
	// After removing PROMO, BIG remains but is not applied (exceeds price)
	if g.TotalDiscount.IncVat != 0 || g.TotalPrice.IncVat != 10000 {
		t.Fatalf("totals incorrect after removal discount=%d total=%d",
			g.TotalDiscount.IncVat, g.TotalPrice.IncVat)
	}

	// Remove not applied
	applyErrorContains(t, reg, g, msgRemoveVoucher(firstId), "not applied")
}

func TestCheckoutMutations(t *testing.T) {
	reg := newRegistry()
	g := newTestGrain()

	applyOK(t, reg, g, msgInitializeCheckout("ORD-1", "PENDING", true))
	if g.OrderReference != "ORD-1" || g.PaymentStatus != "PENDING" || !g.PaymentInProgress {
		t.Fatalf("initialize checkout failed: ref=%s status=%s inProgress=%v",
			g.OrderReference, g.PaymentStatus, g.PaymentInProgress)
	}

	applyOK(t, reg, g, msgOrderCreated("ORD-1", "COMPLETED"))
	if g.OrderReference != "ORD-1" || g.PaymentStatus != "COMPLETED" || g.PaymentInProgress {
		t.Fatalf("order created mutation failed: ref=%s status=%s inProgress=%v",
			g.OrderReference, g.PaymentStatus, g.PaymentInProgress)
	}

	applyErrorContains(t, reg, g, msgInitializeCheckout("", "X", true), "missing orderId")
	applyErrorContains(t, reg, g, msgOrderCreated("", "X"), "missing orderId")
}

func TestSubscriptionDetailsMutation(t *testing.T) {
	reg := newRegistry()
	g := newTestGrain()

	// Upsert new (Id == nil)
	msgNew := &messages.UpsertSubscriptionDetails{
		OfferingCode: "OFF1",
		SigningType:  "TYPE1",
	}
	applyOK(t, reg, g, msgNew)
	if len(g.SubscriptionDetails) != 1 {
		t.Fatalf("expected one subscription detail; got=%d", len(g.SubscriptionDetails))
	}

	// Capture created id
	var createdId string
	for k := range g.SubscriptionDetails {
		createdId = k
	}

	// Update existing
	msgUpdate := &messages.UpsertSubscriptionDetails{
		Id:           &createdId,
		OfferingCode: "OFF2",
		SigningType:  "TYPE2",
	}
	applyOK(t, reg, g, msgUpdate)
	if g.SubscriptionDetails[createdId].OfferingCode != "OFF2" ||
		g.SubscriptionDetails[createdId].SigningType != "TYPE2" {
		t.Fatalf("subscription details not updated: %+v", g.SubscriptionDetails[createdId])
	}

	// Update non-existent
	badId := "NON_EXISTENT"
	applyErrorContains(t, reg, g, &messages.UpsertSubscriptionDetails{Id: &badId}, "not found")

	// Nil mutation should be ignored and produce zero results.
	resultsNil, errNil := reg.Apply(g, (*messages.UpsertSubscriptionDetails)(nil))
	if errNil != nil {
		t.Fatalf("unexpected error for nil mutation element: %v", errNil)
	}
	if len(resultsNil) != 0 {
		t.Fatalf("expected zero results for nil mutation, got %d", len(resultsNil))
	}
}

// Ensure registry Apply handles nil grain and nil message defensive errors consistently.
func TestRegistryDefensiveErrors(t *testing.T) {
	reg := newRegistry()
	g := newTestGrain()

	// Nil grain
	results, err := reg.Apply(nil, &messages.AddItem{})
	if err == nil {
		t.Fatalf("expected error for nil grain")
	}
	if len(results) != 0 {
		t.Fatalf("expected no results for nil grain")
	}

	// Nil message slice
	results, _ = reg.Apply(g, nil)

	if len(results) != 0 {
		t.Fatalf("expected no results when message slice nil")
	}
}
func TestSubscriptionDetailsJSONValidation(t *testing.T) {
	reg := newRegistry()
	g := newTestGrain()

	// Valid JSON on create
	validCreate := &messages.UpsertSubscriptionDetails{
		OfferingCode: "OFFJSON",
		SigningType:  "TYPEJSON",
		Data:         &anypb.Any{Value: []byte(`{"ok":true}`)},
	}
	applyOK(t, reg, g, validCreate)
	if len(g.SubscriptionDetails) != 1 {
		t.Fatalf("expected one subscription detail after valid create, got %d", len(g.SubscriptionDetails))
	}
	var id string
	for k := range g.SubscriptionDetails {
		id = k
	}
	if string(g.SubscriptionDetails[id].Meta) != `{"ok":true}` {
		t.Fatalf("expected meta stored as valid json, got %s", string(g.SubscriptionDetails[id].Meta))
	}

	// Update with valid JSON replaces meta
	updateValid := &messages.UpsertSubscriptionDetails{
		Id:   &id,
		Data: &anypb.Any{Value: []byte(`{"changed":123}`)},
	}
	applyOK(t, reg, g, updateValid)
	if string(g.SubscriptionDetails[id].Meta) != `{"changed":123}` {
		t.Fatalf("expected meta updated to new json, got %s", string(g.SubscriptionDetails[id].Meta))
	}

	// Invalid JSON on create
	invalidCreate := &messages.UpsertSubscriptionDetails{
		OfferingCode: "BAD",
		Data:         &anypb.Any{Value: []byte(`{"broken":}`)},
	}
	res := applyOne(t, reg, g, invalidCreate)
	if res.Error == nil || !strings.Contains(res.Error.Error(), "invalid json") {
		t.Fatalf("expected invalid json error on create, got %v", res.Error)
	}

	// Invalid JSON on update
	badUpdate := &messages.UpsertSubscriptionDetails{
		Id:   &id,
		Data: &anypb.Any{Value: []byte(`{oops`)},
	}
	res2 := applyOne(t, reg, g, badUpdate)
	if res2.Error == nil || !strings.Contains(res2.Error.Error(), "invalid json") {
		t.Fatalf("expected invalid json error on update, got %v", res2.Error)
	}

	// Empty Data.Value should not overwrite existing meta
	emptyUpdate := &messages.UpsertSubscriptionDetails{
		Id:   &id,
		Data: &anypb.Any{Value: []byte{}},
	}
	applyOK(t, reg, g, emptyUpdate)
	if string(g.SubscriptionDetails[id].Meta) != `{"changed":123}` {
		t.Fatalf("empty update should not change meta, got %s", string(g.SubscriptionDetails[id].Meta))
	}
}
