package oobs

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
	"github.com/tech/sendico/pkg/merrors"
	"go.uber.org/zap"
)

func TestRecordAndTimelineQueries(t *testing.T) {
	now := time.Date(2026, time.February, 21, 18, 0, 0, 0, time.UTC)
	metrics := &fakeMetrics{}
	observer := mustObserver(t, Dependencies{
		Logger:  zap.NewNop(),
		Metrics: metrics,
		Now:     func() time.Time { return now },
	})

	payment := &agg.Payment{
		PaymentRef: "pay-1",
		State:      agg.StateExecuting,
		Version:    4,
	}
	if err := observer.RecordPayment(context.Background(), RecordPaymentInput{
		Payment: payment,
		Event:   PaymentEventStateChanged,
		Message: "execution started",
		Fields: map[string]string{
			"quotationRef": "quote-1",
		},
	}); err != nil {
		t.Fatalf("RecordPayment returned error: %v", err)
	}

	step := agg.StepExecution{
		StepRef:  "s1",
		StepCode: "hop.10.crypto.send",
		State:    agg.StepStateRunning,
		Attempt:  1,
	}
	if err := observer.RecordStep(context.Background(), RecordStepInput{
		PaymentRef: payment.PaymentRef,
		Step:       step,
		Event:      StepEventStarted,
		Duration:   1800 * time.Millisecond,
	}); err != nil {
		t.Fatalf("RecordStep(started) returned error: %v", err)
	}

	if err := observer.RecordExternal(context.Background(), RecordExternalInput{
		PaymentRef: payment.PaymentRef,
		StepRef:    step.StepRef,
		Attempt:    1,
		Source:     ExternalSourceGateway,
		Status:     "success",
		RefKind:    "transfer_ref",
		Ref:        "tr-1",
	}); err != nil {
		t.Fatalf("RecordExternal returned error: %v", err)
	}

	step.State = agg.StepStateFailed
	if err := observer.RecordStep(context.Background(), RecordStepInput{
		PaymentRef: payment.PaymentRef,
		Step:       step,
		Event:      StepEventFailed,
		Message:    "terminal failure",
	}); err != nil {
		t.Fatalf("RecordStep(failed) returned error: %v", err)
	}

	paymentTimeline, err := observer.PaymentTimeline(context.Background(), PaymentTimelineInput{
		PaymentRef: payment.PaymentRef,
	})
	if err != nil {
		t.Fatalf("PaymentTimeline returned error: %v", err)
	}
	if len(paymentTimeline.Items) != 4 {
		t.Fatalf("payment timeline size mismatch: got=%d want=4", len(paymentTimeline.Items))
	}
	assertEventOrder(t, paymentTimeline.Items, []string{
		"state_changed",
		"started",
		"external.gateway.success",
		"failed",
	})

	stepTimeline, err := observer.StepTimeline(context.Background(), StepTimelineInput{
		PaymentRef: payment.PaymentRef,
		StepRef:    step.StepRef,
		Attempt:    1,
	})
	if err != nil {
		t.Fatalf("StepTimeline returned error: %v", err)
	}
	if len(stepTimeline.Items) != 3 {
		t.Fatalf("step timeline size mismatch: got=%d want=3", len(stepTimeline.Items))
	}
	assertEventOrder(t, stepTimeline.Items, []string{
		"started",
		"external.gateway.success",
		"failed",
	})

	descTimeline, err := observer.PaymentTimeline(context.Background(), PaymentTimelineInput{
		PaymentRef: payment.PaymentRef,
		Limit:      2,
		Desc:       true,
	})
	if err != nil {
		t.Fatalf("PaymentTimeline(desc) returned error: %v", err)
	}
	if len(descTimeline.Items) != 2 {
		t.Fatalf("desc timeline size mismatch: got=%d want=2", len(descTimeline.Items))
	}
	assertEventOrder(t, descTimeline.Items, []string{"failed", "external.gateway.success"})

	if metrics.paymentEvents != 1 {
		t.Fatalf("payment metric mismatch: got=%d want=1", metrics.paymentEvents)
	}
	if metrics.stepEvents != 2 {
		t.Fatalf("step metric mismatch: got=%d want=2", metrics.stepEvents)
	}
	if metrics.externalEvents != 1 {
		t.Fatalf("external metric mismatch: got=%d want=1", metrics.externalEvents)
	}
	if metrics.stepDurations != 1 {
		t.Fatalf("duration metric mismatch: got=%d want=1", metrics.stepDurations)
	}
}

func TestStepTimeline_AttemptIsolation(t *testing.T) {
	observer := mustObserver(t, Dependencies{Logger: zap.NewNop()})
	ctx := context.Background()

	for _, attempt := range []uint32{1, 2} {
		err := observer.RecordStep(ctx, RecordStepInput{
			PaymentRef: "pay-1",
			Step: agg.StepExecution{
				StepRef:  "s1",
				StepCode: "hop.10.crypto.send",
				State:    agg.StepStateCompleted,
				Attempt:  attempt,
			},
			Event: StepEventCompleted,
		})
		if err != nil {
			t.Fatalf("RecordStep attempt=%d returned error: %v", attempt, err)
		}
	}

	a1, err := observer.StepTimeline(ctx, StepTimelineInput{
		PaymentRef: "pay-1",
		StepRef:    "s1",
		Attempt:    1,
	})
	if err != nil {
		t.Fatalf("StepTimeline(attempt=1) returned error: %v", err)
	}
	if len(a1.Items) != 1 {
		t.Fatalf("attempt=1 timeline size mismatch: got=%d want=1", len(a1.Items))
	}
	if got, want := a1.Items[0].Attempt, uint32(1); got != want {
		t.Fatalf("attempt mismatch: got=%d want=%d", got, want)
	}
}

func TestValidationErrors(t *testing.T) {
	observer := mustObserver(t, Dependencies{Logger: zap.NewNop()})
	ctx := context.Background()

	tests := []struct {
		name string
		run  func() error
	}{
		{
			name: "record payment missing payment",
			run: func() error {
				return observer.RecordPayment(ctx, RecordPaymentInput{
					Event: PaymentEventCreated,
				})
			},
		},
		{
			name: "record payment invalid event",
			run: func() error {
				return observer.RecordPayment(ctx, RecordPaymentInput{
					Payment: &agg.Payment{PaymentRef: "pay-1", State: agg.StateCreated},
					Event:   PaymentEvent("bad"),
				})
			},
		},
		{
			name: "record step missing payment ref",
			run: func() error {
				return observer.RecordStep(ctx, RecordStepInput{
					Step: agg.StepExecution{
						StepRef: "s1",
						State:   agg.StepStatePending,
					},
					Event: StepEventScheduled,
				})
			},
		},
		{
			name: "record step invalid state",
			run: func() error {
				return observer.RecordStep(ctx, RecordStepInput{
					PaymentRef: "pay-1",
					Step: agg.StepExecution{
						StepRef: "s1",
						State:   agg.StepStateUnspecified,
					},
					Event: StepEventScheduled,
				})
			},
		},
		{
			name: "record external invalid source",
			run: func() error {
				return observer.RecordExternal(ctx, RecordExternalInput{
					PaymentRef: "pay-1",
					StepRef:    "s1",
					Attempt:    1,
					Source:     ExternalSource("bad"),
				})
			},
		},
		{
			name: "query step timeline missing attempt",
			run: func() error {
				_, err := observer.StepTimeline(ctx, StepTimelineInput{
					PaymentRef: "pay-1",
					StepRef:    "s1",
				})
				return err
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if err := tt.run(); !errors.Is(err, merrors.ErrInvalidArg) {
				t.Fatalf("expected invalid argument error, got %v", err)
			}
		})
	}
}

func assertEventOrder(t *testing.T, items []TimelineEntry, expected []string) {
	t.Helper()
	if len(items) != len(expected) {
		t.Fatalf("event count mismatch: got=%d want=%d", len(items), len(expected))
	}
	for i := range expected {
		if got, want := items[i].Event, expected[i]; got != want {
			t.Fatalf("event[%d] mismatch: got=%q want=%q", i, got, want)
		}
	}
}

func mustObserver(t *testing.T, deps Dependencies) Observer {
	t.Helper()
	out, err := New(deps)
	if err != nil {
		t.Fatalf("New returned error: %v", err)
	}
	return out
}

type fakeMetrics struct {
	paymentEvents  int
	stepEvents     int
	externalEvents int
	stepDurations  int
}

func (f *fakeMetrics) IncPaymentEvent(_ PaymentEvent, _ agg.State) {
	f.paymentEvents++
}

func (f *fakeMetrics) IncStepEvent(_ StepEvent, _ string, _ agg.StepState) {
	f.stepEvents++
}

func (f *fakeMetrics) IncExternalEvent(_ ExternalSource, _ string) {
	f.externalEvents++
}

func (f *fakeMetrics) ObserveStepDuration(_ string, _ agg.StepState, _ time.Duration) {
	f.stepDurations++
}
