package distribution

import (
	"context"
	"errors"
	"regexp"
	"strings"
	"testing"
	"time"

	"github.com/shopspring/decimal"
)

type fakeProvider struct {
	name   string
	active bool
	live   *LiveLocation
}

func (p *fakeProvider) Name() string { return p.name }
func (p *fakeProvider) Active() bool { return p.active }
func (p *fakeProvider) Latest(context.Context, int64) (*LiveLocation, error) {
	if p.live == nil {
		return nil, ErrNotFound
	}
	return p.live, nil
}

// seedShipment builds the SO -> DO -> shipment chain and returns the shipment.
func seedShipment(t *testing.T, svc *Service, f *fakeStore) *Shipment {
	t.Helper()
	f.mustSO(t, 1, 1, "confirmed", 1)
	f.addStock(1, 1, decimal.NewFromInt(770), decimal.NewFromInt(770))
	do := f.mustDO(t, svc, 1, 1)
	sh, err := svc.CreateShipment(context.Background(), do.ID, ShipmentInput{
		Items: []ShipmentItemInput{{ProductID: 1, ShippedQuantity: decimal.NewFromInt(100)}},
	}, 1)
	if err != nil {
		t.Fatalf("CreateShipment: %v", err)
	}
	return sh
}

func monthCode(t *testing.T) string {
	t.Helper()
	return time.Now().In(jakarta()).Format("200601")
}

func TestRecordTrackingEventValidation(t *testing.T) {
	f := newFakeStore()
	svc := newService(f)
	sh := seedShipment(t, svc, f)
	ctx := context.Background()

	base := TrackingEventInput{Status: ShipmentInTransit, Source: TrackingSourceManual}

	cases := []struct {
		name string
		mut  func(*TrackingEventInput)
		want error
	}{
		{"bad status", func(in *TrackingEventInput) { in.Status = "lost" }, ErrTrackingStatusInvalid},
		{"bad source", func(in *TrackingEventInput) { in.Source = "carrier_pigeon" }, ErrTrackingSourceInvalid},
		{"lat without lng", func(in *TrackingEventInput) {
			lat := -6.2
			in.Latitude = &lat
		}, ErrTrackingCoordsInvalid},
		{"lat out of range", func(in *TrackingEventInput) {
			lat, lng := 91.0, 106.0
			in.Latitude, in.Longitude = &lat, &lng
		}, ErrTrackingCoordsInvalid},
		{"lng out of range", func(in *TrackingEventInput) {
			lat, lng := -6.2, 181.0
			in.Latitude, in.Longitude = &lat, &lng
		}, ErrTrackingCoordsInvalid},
		{"negative accuracy", func(in *TrackingEventInput) {
			acc := -1.0
			in.AccuracyM = &acc
		}, ErrTrackingAccuracy},
		{"future event time", func(in *TrackingEventInput) {
			in.EventTime = time.Now().Add(10 * time.Minute).UTC().Format(time.RFC3339)
		}, ErrTrackingEventTime},
		{"unparsable event time", func(in *TrackingEventInput) { in.EventTime = "yesterday" }, ErrTrackingEventTime},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			in := base
			tc.mut(&in)
			if _, err := svc.RecordTrackingEvent(ctx, sh.ID, in, 1); !errors.Is(err, tc.want) {
				t.Fatalf("err = %v, want %v", err, tc.want)
			}
		})
	}
	if got := len(f.trackingEvents[sh.ID]); got != 0 {
		t.Fatalf("events stored = %d, want 0 (validation must not write)", got)
	}
}

func TestRecordTrackingEventAppendAndDedupe(t *testing.T) {
	f := newFakeStore()
	svc := newService(f)
	sh := seedShipment(t, svc, f)
	ctx := context.Background()

	lat, lng, acc := -6.2, 106.8, 12.5
	ref := "tsp-123"
	ev, err := svc.RecordTrackingEvent(ctx, sh.ID, TrackingEventInput{
		Status:      ShipmentInTransit,
		Source:      TrackingSourceThirdParty,
		Latitude:    &lat,
		Longitude:   &lng,
		AccuracyM:   &acc,
		Note:        ptr("crossing cikampek"),
		ProviderRef: &ref,
	}, 1)
	if err != nil {
		t.Fatalf("record: %v", err)
	}
	if ev.ID == 0 || ev.Latitude == nil || *ev.Latitude != lat || ev.Source != TrackingSourceThirdParty {
		t.Fatalf("event = %+v", ev)
	}
	if ev.Note == nil || *ev.Note != "crossing cikampek" {
		t.Fatalf("note = %v", ev.Note)
	}

	dup, err := svc.RecordTrackingEvent(ctx, sh.ID, TrackingEventInput{
		Status:      ShipmentInTransit,
		Source:      TrackingSourceThirdParty,
		ProviderRef: &ref,
	}, 1)
	if err != nil {
		t.Fatalf("duplicate record: %v", err)
	}
	if dup.ID != ev.ID {
		t.Fatalf("duplicate returned id %d, want existing %d", dup.ID, ev.ID)
	}
	if got := len(f.trackingEvents[sh.ID]); got != 1 {
		t.Fatalf("events stored = %d, want 1 after dedupe", got)
	}
}

func TestGetShipmentTrackingNoLiveSource(t *testing.T) {
	f := newFakeStore()
	svc := newService(f)
	sh := seedShipment(t, svc, f)
	ctx := context.Background()

	lat, lng := -6.2, 106.8
	if _, err := svc.RecordTrackingEvent(ctx, sh.ID, TrackingEventInput{
		Status: ShipmentPending, Source: TrackingSourceManual, Latitude: &lat, Longitude: &lng,
	}, 1); err != nil {
		t.Fatalf("record: %v", err)
	}
	sum, err := svc.GetShipmentTracking(ctx, sh.ID)
	if err != nil {
		t.Fatalf("tracking: %v", err)
	}
	if sum.RealTime.Available {
		t.Fatal("real_time.available = true without a provider")
	}
	if sum.Latest == nil || sum.Latest.IsLive || sum.Latest.Latitude != lat || sum.Latest.Source != TrackingSourceManual {
		t.Fatalf("latest = %+v, want historical point", sum.Latest)
	}
	if len(sum.History) != 1 {
		t.Fatalf("history = %d events, want 1", len(sum.History))
	}
	if sum.Shipment.ShipmentNumber != sh.ShipmentNumber {
		t.Fatalf("shipment number = %s, want %s", sum.Shipment.ShipmentNumber, sh.ShipmentNumber)
	}
}

func TestGetShipmentTrackingWithLiveProvider(t *testing.T) {
	f := newFakeStore()
	svc := newService(f)
	sh := seedShipment(t, svc, f)
	provider := &fakeProvider{
		name:   "driver-gps",
		active: true,
		live: &LiveLocation{
			Latitude: -6.3, Longitude: 106.9, AccuracyM: 3,
			ObservedAt: time.Now(), Source: TrackingSourceDriverMobile,
		},
	}
	svc.WithLocationProvider(provider)

	sum, err := svc.GetShipmentTracking(context.Background(), sh.ID)
	if err != nil {
		t.Fatalf("tracking: %v", err)
	}
	if !sum.RealTime.Available || sum.RealTime.Provider == nil || *sum.RealTime.Provider != "driver-gps" {
		t.Fatalf("real_time = %+v, want live provider", sum.RealTime)
	}
	if sum.Latest == nil || !sum.Latest.IsLive || sum.Latest.Latitude != -6.3 {
		t.Fatalf("latest = %+v, want live point", sum.Latest)
	}

	provider.active = false
	sum, err = svc.GetShipmentTracking(context.Background(), sh.ID)
	if err != nil {
		t.Fatalf("tracking: %v", err)
	}
	if sum.RealTime.Available {
		t.Fatal("real_time.available = true with inactive provider")
	}
	if sum.Latest != nil {
		t.Fatalf("latest = %+v, want nil (no events recorded)", sum.Latest)
	}
}

func TestLookupShipmentIdentifiers(t *testing.T) {
	f := newFakeStore()
	svc := newService(f)
	ctx := context.Background()

	// two DOs, one shipment each -> distinct barcodes/numbers
	f.mustSO(t, 1, 1, "confirmed", 1)
	f.mustSO(t, 2, 1, "confirmed", 1)
	f.addStock(1, 1, decimal.NewFromInt(770), decimal.NewFromInt(770))
	do1 := f.mustDO(t, svc, 1, 1)
	do2 := f.mustDO(t, svc, 2, 1)
	sh1, err := svc.CreateShipment(ctx, do1.ID, ShipmentInput{
		Items: []ShipmentItemInput{{ProductID: 1, ShippedQuantity: decimal.NewFromInt(100)}},
	}, 1)
	if err != nil {
		t.Fatalf("shipment 1: %v", err)
	}
	sh2, err := svc.CreateShipment(ctx, do2.ID, ShipmentInput{
		Items: []ShipmentItemInput{{ProductID: 1, ShippedQuantity: decimal.NewFromInt(100)}},
	}, 1)
	if err != nil {
		t.Fatalf("shipment 2: %v", err)
	}
	mc := monthCode(t)
	formats := map[string]string{
		"shipment_number": sh1.ShipmentNumber, "tracking_number": sh1.TrackingNumber,
		"barcode": sh1.Barcode, "do_number": do1.DoNumber,
	}
	wantPrefix := map[string]string{"shipment_number": "SHP", "tracking_number": "TRK", "barcode": "BAR", "do_number": "DO"}
	for family, v := range formats {
		re := regexp.MustCompile(`^` + wantPrefix[family] + `-\d{6}-\d{6}$`)
		if !re.MatchString(v) || !strings.HasPrefix(v, wantPrefix[family]+"-"+mc+"-") {
			t.Fatalf("%s = %q, want %s-%s-NNNNNN", family, v, wantPrefix[family], mc)
		}
	}
	if sh1.ShipmentNumber == sh2.ShipmentNumber || sh1.TrackingNumber == sh2.TrackingNumber ||
		sh1.Barcode == sh2.Barcode || sh1.ShipmentNumber == sh1.TrackingNumber {
		t.Fatalf("identifiers must be distinct: %s %s %s %s", sh1.ShipmentNumber, sh1.TrackingNumber, sh1.Barcode, sh2.ShipmentNumber)
	}
	if sh1.ShipmentNumber == sh1.Barcode || sh1.TrackingNumber == sh1.Barcode {
		t.Fatalf("barcode %s must not equal shipment/tracking numbers", sh1.Barcode)
	}

	lookups := []struct {
		q         string
		matchedBy string
		count     int
		wantID    int64
	}{
		{do1.DoNumber, "do_number", 1, sh1.ID},
		{sh1.ShipmentNumber, "shipment_number", 1, sh1.ID},
		{sh1.TrackingNumber, "tracking_number", 1, sh1.ID},
		{sh1.Barcode, "barcode", 1, sh1.ID},
		{do2.DoNumber, "do_number", 1, sh2.ID},
		{sh2.Barcode, "barcode", 1, sh2.ID},
	}
	for _, tc := range lookups {
		t.Run(tc.q, func(t *testing.T) {
			res, err := svc.LookupShipment(ctx, tc.q)
			if err != nil {
				t.Fatalf("lookup: %v", err)
			}
			if res.MatchedBy != tc.matchedBy || len(res.Shipments) != tc.count || res.Shipments[0].Shipment.ID != tc.wantID {
				t.Fatalf("lookup %s = matched %s count %d want %d, got %+v", tc.q, res.MatchedBy, len(res.Shipments), tc.count, res.Shipments)
			}
		})
	}

	if _, err := svc.LookupShipment(ctx, "DOES-NOT-EXIST"); !errors.Is(err, ErrNotFound) {
		t.Fatalf("garbage lookup err = %v, want ErrNotFound", err)
	}
	if _, err := svc.LookupShipment(ctx, "  "); !errors.Is(err, ErrIdentifierRequired) {
		t.Fatalf("empty lookup err = %v, want ErrIdentifierRequired", err)
	}
}

func TestRecordTrackingEventMissingShipment(t *testing.T) {
	f := newFakeStore()
	svc := newService(f)
	_, err := svc.RecordTrackingEvent(context.Background(), 999, TrackingEventInput{
		Status: ShipmentPending, Source: TrackingSourceManual,
	}, 1)
	if !errors.Is(err, ErrNotFound) {
		t.Fatalf("err = %v, want ErrNotFound", err)
	}
}

func ptr(s string) *string { return &s }
