package hr

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

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

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

// monthBucketSeq models pkg/seq.Next: the counter lives in a bucket keyed on
// (prefix, doc_date) and resets when the date changes. The payroll allocator's
// date is the first of the payroll PERIOD, so a run for the same month draws
// from the same bucket and a different month draws from a different one.
type monthBucketSeq struct {
	mu      sync.Mutex
	buckets map[string]int64
	calls   []time.Time
}

func newMonthBucketSeq() *monthBucketSeq {
	return &monthBucketSeq{buckets: map[string]int64{}}
}

func (m *monthBucketSeq) Sequence(_ context.Context, prefix string, date time.Time, _ any) (int64, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	// Keyed on the value's OWN location, which is what pgx encodes into a DATE
	// column (pgtype/date.go PlanEncode reads Year/Month/Day off the time.Time
	// as given and does not convert to UTC). Formatting in UTC here would model
	// a driver that is not the one in use, and midnight WIB would look like the
	// previous day.
	key := prefix + "|" + date.Format("2006-01-02")
	m.buckets[key]++
	m.calls = append(m.calls, date)
	return m.buckets[key], nil
}

// payrollStore models the REAL behaviour of the payroll write, which the
// allocator tests depend on being accurate about:
//
//   - payrolls_payroll_number_key is UNIQUE, so a number already held by a
//     DIFFERENT row is a real 23505;
//   - UpsertPayroll is ON CONFLICT (employee_id, period_month, period_year)
//     DO UPDATE, and payroll_number is NOT in the SET list, so re-running a
//     period for an employee who already has a row for it keeps that row's
//     ORIGINAL number and never inserts.
//
// That second point is why a naive "the store rejects every duplicate" fake
// gives the wrong answer for a second run of the same period.
type payrollStore struct {
	*fakeStore
	mu sync.Mutex
	// rows is keyed by employee+period, holding the number that row kept.
	rows map[string]string
	// holders maps a number to the employee+period key that holds it.
	holders map[string]string
	nextID  int64
}

func newPayrollStore() *payrollStore {
	return &payrollStore{
		fakeStore: newFakeStore(),
		rows:      map[string]string{},
		holders:   map[string]string{},
		nextID:    1,
	}
}

// addEmployee seeds an active employee.
func (s *payrollStore) addEmployee(id int64, name string) {
	s.emps[id] = db.GetEmployeeByIDRow{ID: id, Name: name, IsActive: true,
		BasicSalary: decimal.NewFromInt(4000000)}
}

// setActive deactivates or reactivates an employee. A payslip already written
// for a period is NOT removed when an employee leaves - that is the whole
// reason a departed employee's ordinal can still be handed to somebody else.
func (s *payrollStore) setActive(id int64, active bool) {
	if e, ok := s.emps[id]; ok {
		e.IsActive = active
		s.emps[id] = e
	}
}

// ActiveEmployees overrides the shared fake to reproduce the ORDER BY id that
// ListActiveEmployees has in SQL (internal/db/hr.sql.go). The shared fake walks
// a Go map, whose iteration order is randomised, so without this the
// employee-to-ordinal assignment is nondeterministic and these tests are flaky.
func (s *payrollStore) ActiveEmployees(context.Context) ([]db.ListActiveEmployeesRow, error) {
	ids := make([]int64, 0, len(s.emps))
	for id, e := range s.emps {
		if e.IsActive {
			ids = append(ids, id)
		}
	}
	sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
	out := make([]db.ListActiveEmployeesRow, 0, len(ids))
	for _, id := range ids {
		out = append(out, db.ListActiveEmployeesRow(s.emps[id]))
	}
	return out, nil
}

func periodKey(employeeID int64, month, year int16) string {
	return fmt.Sprintf("%d/%d/%d", employeeID, month, year)
}

func (s *payrollStore) UpsertPayroll(_ context.Context, p db.UpsertPayrollParams) (db.UpsertPayrollRow, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	key := periodKey(p.EmployeeID, p.PeriodMonth, p.PeriodYear)

	if existing, ok := s.rows[key]; ok {
		// ON CONFLICT DO UPDATE: the row keeps the number it already had,
		// because payroll_number is not in the SET list.
		return db.UpsertPayrollRow{
			ID: s.nextID, PayrollNumber: existing, EmployeeID: p.EmployeeID,
			PeriodMonth: p.PeriodMonth, PeriodYear: p.PeriodYear,
			NetSalary: p.NetSalary, Status: "draft",
		}, nil
	}

	// A genuine INSERT: the number must not be held by some other row. The
	// repository maps 23505 to ErrDuplicate, so the fake does too - the service
	// under test branches on that sentinel.
	if holder, taken := s.holders[p.PayrollNumber]; taken && holder != key {
		return db.UpsertPayrollRow{}, mapWriteErr(&pgconn.PgError{
			Severity: "ERROR",
			Code:     "23505",
			Message: fmt.Sprintf(
				`duplicate key value violates unique constraint "payrolls_payroll_number_key" (%s)`,
				p.PayrollNumber),
			ConstraintName: "payrolls_payroll_number_key",
			TableName:      "payrolls",
		})
	}
	s.rows[key] = p.PayrollNumber
	s.holders[p.PayrollNumber] = key
	s.nextID++
	return db.UpsertPayrollRow{
		ID: s.nextID, PayrollNumber: p.PayrollNumber, EmployeeID: p.EmployeeID,
		PeriodMonth: p.PeriodMonth, PeriodYear: p.PeriodYear,
		NetSalary: p.NetSalary, Status: "draft",
	}, nil
}

// TestPayrollNumber_TwoRunsSameMonthUseOneCounter: the fix must make the
// counter per payroll period. Two runs for the same month have to draw distinct
// ordinals, because the month in the number is already fixed and the ordinal is
// the only thing separating them.
func TestPayrollNumber_TwoRunsSameMonthUseOneCounter(t *testing.T) {
	seq := newMonthBucketSeq()
	store := newPayrollStore()
	store.addEmployee(1, "Rina")
	store.addEmployee(2, "Sari")
	svc := newSvcWithSeq(store, seq, time.FixedZone("WIB", 7*3600))

	first, err := svc.GeneratePayrolls(context.Background(), 9, 2026)
	if err != nil {
		t.Fatalf("first run failed: %v", err)
	}
	if len(first) != 2 {
		t.Fatalf("first run produced %d payrolls, want 2", len(first))
	}

	// A third employee joins and the period is re-run. The re-run must not
	// collide: the counter for the period has moved on, and the two existing
	// employees keep the numbers their rows already had.
	store.addEmployee(3, "Dewi")
	if _, err := svc.GeneratePayrolls(context.Background(), 9, 2026); err != nil {
		t.Fatalf("a re-run of the same period collided: %v", err)
	}

	// Every number actually stored must name the period it was generated for.
	for num := range store.holders {
		if !strings.HasPrefix(num, "PAY-202609-") {
			t.Errorf("stored number %q does not carry the period it was generated for", num)
		}
	}

	// Every allocation - there is now one per payslip rather than one per run -
	// must have asked for the SAME bucket: ('pay', 2026-09-01).
	if len(seq.calls) == 0 {
		t.Fatal("the allocator was never called")
	}
	for i, c := range seq.calls {
		if got := c.Format("2006-01-02"); got != "2026-09-01" {
			t.Errorf("allocation %d bucket date = %s, want 2026-09-01 (the payroll period, not the clock)", i+1, got)
		}
	}

	// The two runs must not have handed the same ordinal to two different
	// employees in the same period.
	if len(store.holders) != 3 {
		t.Errorf("distinct payroll numbers = %d, want 3 (two from the first run, one for the new employee)", len(store.holders))
	}
}

// TestPayrollNumber_BucketFollowsThePeriodNotTheClock is the test that pins the
// distinction that makes PAY different from EMP- and from cashier/galon.
//
// PAY-202609-0001 is routinely generated in October, for the September period.
// If the bucket were keyed on the current month, the same September period would
// draw from two different counters depending on the day it was run, and a
// September period run in September and then again in October would collide.
func TestPayrollNumber_BucketFollowsThePeriodNotTheClock(t *testing.T) {
	seq := newMonthBucketSeq()
	store := newPayrollStore()
	store.addEmployee(1, "Rina")
	store.addEmployee(2, "Sari")
	loc := time.FixedZone("WIB", 7*3600)

	// Two different wall-clock days, one payroll period.
	day := time.Date(2026, 9, 30, 10, 0, 0, 0, loc)
	svc := newSvcWithSeq(store, seq, loc)
	svc.now = func() time.Time { return day }

	if _, err := svc.GeneratePayrolls(context.Background(), 9, 2026); err != nil {
		t.Fatalf("September run in September failed: %v", err)
	}

	day = time.Date(2026, 10, 5, 10, 0, 0, 0, loc)

	// A new employee joins and the September period is run again, this time in
	// October. The counter for September must have moved on, because the bucket
	// is the period and not the day.
	store.addEmployee(3, "Dewi")
	if _, err := svc.GeneratePayrolls(context.Background(), 9, 2026); err != nil {
		t.Fatalf("September run in October collided, which means the bucket followed the clock: %v", err)
	}

	// All allocations across both runs, and across two different wall-clock
	// days, must be the same bucket: the period, not the day.
	if len(seq.calls) == 0 {
		t.Fatal("the allocator was never called")
	}
	for i, c := range seq.calls {
		if !c.Equal(seq.calls[0]) {
			t.Errorf("allocation %d used bucket %v, want %v for every run of the same period",
				i+1, c, seq.calls[0])
		}
	}
	if len(store.holders) != 3 {
		t.Errorf("distinct payroll numbers = %d, want 3", len(store.holders))
	}
}

// TestPayrollNumber_DifferentMonthsUseDifferentBuckets: the converse. A new
// period must start its own ordinal, because the month in the number already
// distinguishes it and a shared counter would leave gaps that mean nothing.
func TestPayrollNumber_DifferentMonthsUseDifferentBuckets(t *testing.T) {
	seq := newMonthBucketSeq()
	store := newPayrollStore()
	store.addEmployee(1, "Rina")
	svc := newSvcWithSeq(store, seq, time.FixedZone("WIB", 7*3600))

	sept, err := svc.GeneratePayrolls(context.Background(), 9, 2026)
	if err != nil {
		t.Fatalf("September run failed: %v", err)
	}
	oct, err := svc.GeneratePayrolls(context.Background(), 10, 2026)
	if err != nil {
		t.Fatalf("October run failed: %v", err)
	}

	if got := sept[0].PayrollNumber; !strings.HasPrefix(got, "PAY-202609-") {
		t.Errorf("September number = %q, want the PAY-202609- prefix", got)
	}
	if got := oct[0].PayrollNumber; !strings.HasPrefix(got, "PAY-202610-") {
		t.Errorf("October number = %q, want the PAY-202610- prefix", got)
	}
	// Each month's ordinal restarts at 1, so the two numbers differ only in
	// the month they name.
	if sept[0].PayrollNumber[len("PAY-202609-"):] != "0001" {
		t.Errorf("September ordinal = %q, want it to restart at 0001 for a new period",
			sept[0].PayrollNumber[len("PAY-202609-"):])
	}
	if len(seq.calls) < 2 {
		t.Fatalf("allocator called %d times, want at least one per period", len(seq.calls))
	}
	if seq.calls[0].Equal(seq.calls[len(seq.calls)-1]) {
		t.Errorf("two different periods shared a bucket: %v", seq.calls[0])
	}
	if got := seq.calls[0].Format("2006-01-02"); got != "2026-09-01" {
		t.Errorf("September bucket = %s, want 2026-09-01", got)
	}
	last := seq.calls[len(seq.calls)-1]
	if got := last.Format("2006-01-02"); got != "2026-10-01" {
		t.Errorf("October bucket = %s, want 2026-10-01", got)
	}
}

// TestPayrollBucketDate: the bucket date is a pure function of (year, month). If
// it ever started depending on the clock again, every test above would be a
// coincidence rather than a guarantee.
func TestPayrollBucketDate(t *testing.T) {
	loc := time.FixedZone("WIB", 7*3600)
	got := payrollBucketDate(2026, 9, loc)
	if y, m, d := got.Date(); y != 2026 || m != time.September || d != 1 {
		t.Errorf("payrollBucketDate(2026, 9) = %s, want 2026-09-01", got.Format("2006-01-02"))
	}
	if got.Location() != loc {
		t.Error("bucket date is not in the service's location")
	}
	// The 31st must collapse to the 1st, not overflow into the next month.
	if y, m, d := payrollBucketDate(2026, 12, loc).Date(); y != 2026 || m != time.December || d != 1 {
		t.Errorf("payrollBucketDate(2026, 12) = %s, want 2026-12-01",
			payrollBucketDate(2026, 12, loc).Format("2006-01-02"))
	}
}
