package warehouses

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/jackc/pgx/v5/pgconn"

	"github.com/TaruLab/keluargamart-backend/internal/db"
)

// bucketSeq models pkg/seq.Next faithfully: the counter lives in a bucket keyed
// on (prefix, doc_date) and RESETS when the date changes. That reset is the
// defect under test, so a fake that ignored the date would hide it.
type bucketSeq struct {
	mu      sync.Mutex
	buckets map[string]int64
	calls   []time.Time
}

// newBucketSeqAt models the state migration 000107 leaves behind: the anchor
// bucket already above any existing GDG- rows. A per-day bucket has no such
// starting point - it opens empty at 1.
func newBucketSeqAt(start int64) *bucketSeq {
	return &bucketSeq{buckets: map[string]int64{"GDG|2000-01-01": start - 1}}
}

func (b *bucketSeq) Sequence(_ context.Context, prefix string, date time.Time, _ any) (int64, error) {
	b.mu.Lock()
	defer b.mu.Unlock()
	key := prefix + "|" + date.UTC().Format("2006-01-02")
	b.buckets[key]++
	b.calls = append(b.calls, date)
	return b.buckets[key], nil
}

// uniqueStore rejects a duplicate code the way warehouses_code_key does, so a
// collision surfaces as an error instead of a silently-duplicated row.
type uniqueStore struct {
	*fakeStore
	mu     sync.Mutex
	taken  map[string]bool
	byCode map[string]int64
}

func newUniqueStore() *uniqueStore {
	return &uniqueStore{
		fakeStore: newFakeStore(),
		taken:     map[string]bool{},
		byCode:    map[string]int64{},
	}
}

func (s *uniqueStore) Create(_ context.Context, w *db.Warehouse) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.taken[w.Code] {
		// The shape the driver returns for the unique index on warehouses.code.
		return &pgconn.PgError{
			Severity: "ERROR",
			Code:     "23505",
			Message: fmt.Sprintf(`duplicate key value violates unique constraint "warehouses_code_key" (%s)`,
				w.Code),
			ConstraintName: "warehouses_code_key",
			TableName:      "warehouses",
		}
	}
	s.taken[w.Code] = true
	s.byCode[w.Code] = int64(len(s.byCode) + 1)
	return s.fakeStore.Create(context.Background(), w)
}

func newSvcWithSeq(s Store, sq Seq, loc *time.Location) *Service {
	return NewService(s, sq, loc)
}

// TestWarehouseCode_DoesNotResetAcrossDays is the regression test for the GDG
// half of the allocator bug. Same shape as the EMP- one: the code carries no
// date but was allocated from today's bucket, so the counter restarted at 1
// every midnight and the next warehouse was handed GDG-001 again.
//
// Against the daily-bucket code this fails on the second create.
func TestWarehouseCode_DoesNotResetAcrossDays(t *testing.T) {
	// seed.sql creates WH1..WH3 and WH-KM-*, never a GDG- code, so a real
	// database has no GDG- rows to collide with on day one. The bug is
	// therefore seeded explicitly here, which is what a live database would
	// have after a warehouse was created through the ERP.
	seq := newBucketSeqAt(1)
	store := newUniqueStore()
	loc := time.FixedZone("WIB", 7*3600)

	day := time.Date(2026, 3, 10, 9, 0, 0, 0, loc)
	svc := newSvcWithSeq(store, seq, loc)
	svc.now = func() time.Time { return day }

	w1, err := svc.Create(context.Background(), Input{Name: "Gudang Pusat"})
	if err != nil {
		t.Fatalf("day 1 create failed: %v", err)
	}
	if !strings.HasPrefix(w1.Code, "GDG-") {
		t.Fatalf("code = %q, want a GDG- code", w1.Code)
	}

	// Midnight passes. The next create must not restart the counter.
	day = day.AddDate(0, 0, 1)

	w2, err := svc.Create(context.Background(), Input{Name: "Gudang Transit"})
	if err != nil {
		t.Fatalf("a create on the NEXT DAY collided, which is the live bug: %v", err)
	}
	if w1.Code == w2.Code {
		t.Errorf("both days were allocated %s: the counter reset at midnight", w1.Code)
	}
	if len(store.byCode) != 2 {
		t.Errorf("distinct warehouse codes = %d, want 2", len(store.byCode))
	}

	// The allocator must have been asked for one bucket, twice, not a bucket
	// per day.
	if len(seq.calls) != 2 {
		t.Fatalf("allocator called %d times, want 2", len(seq.calls))
	}
	if got := seq.calls[0].UTC().Format("2006-01-02"); got != "2000-01-01" {
		t.Errorf("bucket date = %s, want the fixed anchor 2000-01-01 so the counter never resets", got)
	}
	if !seq.calls[0].Equal(seq.calls[1]) {
		t.Errorf("allocator bucket date changed between creates: %s then %s",
			seq.calls[0].Format("2006-01-02"), seq.calls[1].Format("2006-01-02"))
	}
}

// TestWarehouseCode_SeededCodeIsNotReissued: the anchor bucket must start above
// whatever GDG- codes already exist.
//
// This is the state migration 000107 creates. It is asserted here because the
// code fix on its own is NOT sufficient: moving allocation to the anchor bucket
// on a database that already holds GDG-001..GDG-00n would hand out GDG-001
// again, because that bucket starts empty at 1. The backfill is what lifts it,
// and without the backfill this exact collision returns one deployment later.
func TestWarehouseCode_SeededCodeIsNotReissued(t *testing.T) {
	// One GDG- code exists, so the backfill would have set the counter to 1
	// and the next allocation is 2.
	seq := newBucketSeqAt(2)
	store := newUniqueStore()
	loc := time.FixedZone("WIB", 7*3600)
	store.taken["GDG-001"] = true

	svc := newSvcWithSeq(store, seq, loc)
	w, err := svc.Create(context.Background(), Input{Name: "Gudang Baru"})
	if err != nil {
		t.Fatalf("create failed: %v", err)
	}
	if w.Code == "GDG-001" {
		t.Error("allocated GDG-001, which already exists: the anchor bucket is below the data")
	}
	if w.Code != "GDG-002" {
		t.Errorf("code = %q, want GDG-002", w.Code)
	}
}

// TestWarehouseCode_ConcurrentCreatesAreDistinct actually races. The allocator
// is contractually atomic, so this passes before and after the fix; it is here
// to prove that moving to one shared bucket did not INTRODUCE a race, which is
// the real hazard of consolidating away from per-day buckets.
func TestWarehouseCode_ConcurrentCreatesAreDistinct(t *testing.T) {
	seq := newBucketSeqAt(1)
	store := newUniqueStore()
	svc := newSvcWithSeq(store, seq, time.FixedZone("WIB", 7*3600))

	const workers = 8
	var ready sync.WaitGroup
	var wg sync.WaitGroup
	ready.Add(workers)
	wg.Add(workers)
	start := make(chan struct{})

	errs := make([]error, workers)
	for i := 0; i < workers; i++ {
		go func(i int) {
			defer wg.Done()
			ready.Done()
			<-start
			_, errs[i] = svc.Create(context.Background(), Input{Name: fmt.Sprintf("Gudang %d", i)})
		}(i)
	}
	ready.Wait() // barrier: all workers are parked before any is released
	close(start)
	wg.Wait()

	for i, err := range errs {
		if err != nil {
			t.Errorf("worker %d failed: %v", i, err)
		}
	}
	if len(store.byCode) != workers {
		t.Errorf("distinct warehouse codes = %d, want %d", len(store.byCode), workers)
	}
}
