package hr

import (
	"context"
	"errors"
	"testing"

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

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

// asUser builds a context carrying an authenticated identity, which is what
// resolveLeaveTarget reads the permission from. hr_staff holds hr.leaves.* so
// it gains hr.leaves.store.any through the existing glob (no migration);
// `employee` holds only the exact hr.leaves.store and must not gain it.
func asUser(role auth.Role, perms map[string]bool) context.Context {
	p := auth.Permissions{}
	for k, v := range perms {
		p[k] = v
	}
	return auth.ToContext(context.Background(), &auth.User{ID: 5, Role: role, Permissions: p}, 5)
}

func seedEmployee(f *fakeStore, id int64, name string, userID int64) {
	row := db.GetEmployeeByIDRow{
		ID:             id,
		Name:           name,
		Department:     "hr",
		JobLevel:       "staff",
		EmploymentType: "permanent",
		// Approval checks the balance, so a seeded employee needs one.
		LeaveBalance: decimal.NewFromInt(30),
		IsActive:     true,
	}
	if userID > 0 {
		row.UserID = pgtype.Int8{Int64: userID, Valid: true}
	}
	f.emps[id] = row
}

func leaveInput(employeeID int64) LeaveInput {
	return LeaveInput{
		EmployeeID: employeeID,
		LeaveType:  "annual",
		StartDate:  "2026-11-02",
		EndDate:    "2026-11-03",
		Reason:     "test",
	}
}

// The ERP sends a required Karyawan selection. Before the fix the service
// ignored employee_id and filed against the caller, so every pick landed on the
// wrong person with a success toast.
func TestCreateLeave_FilesForTheSelectedEmployee(t *testing.T) {
	f := newFakeStore()
	seedEmployee(f, 1, "HR Staff", 5)
	seedEmployee(f, 2, "Other Employee", 0)
	svc := newSvc(f)

	ctx := asUser(auth.RoleHRStaff, map[string]bool{LeaveTargetAnyPermission: true})
	got, err := svc.CreateLeave(ctx, 5, leaveInput(2))
	if err != nil {
		t.Fatalf("CreateLeave: %v", err)
	}
	if got.EmployeeID != 2 {
		t.Fatalf("employee_id = %d, want 2 (the selected employee)", got.EmployeeID)
	}
}

func TestCreateLeave_SelfFileNeedsNoExtraPermission(t *testing.T) {
	f := newFakeStore()
	seedEmployee(f, 1, "Driver", 5)
	svc := newSvc(f)

	// A plain employee: exact hr.leaves.store only, no .any.
	ctx := asUser(auth.RoleEmployee, map[string]bool{"hr.leaves.store": true})

	for _, id := range []int64{0, 1} {
		got, err := svc.CreateLeave(ctx, 5, leaveInput(id))
		if err != nil {
			t.Fatalf("self-file with employee_id=%d: %v", id, err)
		}
		if got.EmployeeID != 1 {
			t.Fatalf("employee_id = %d, want 1 (the caller)", got.EmployeeID)
		}
	}
}

func TestCreateLeave_RejectsFilingForAnotherEmployeeWithoutPermission(t *testing.T) {
	f := newFakeStore()
	seedEmployee(f, 1, "Driver", 5)
	seedEmployee(f, 2, "Someone Else", 0)
	svc := newSvc(f)

	ctx := asUser(auth.RoleEmployee, map[string]bool{"hr.leaves.store": true})
	_, err := svc.CreateLeave(ctx, 5, leaveInput(2))
	if !errors.Is(err, ErrLeaveForOther) {
		t.Fatalf("err = %v, want ErrLeaveForOther", err)
	}
	if len(f.leaves) != 0 {
		t.Fatalf("a leave was persisted despite the refusal: %d", len(f.leaves))
	}
}

// Same refusal, same status, whether or not the caller has their own employee
// row. Returning ErrEmployeeRequired (404) here would tell the caller the
// employee they named does not exist, which is not what went wrong.
func TestCreateLeave_NoOwnEmployeeRowStillAnswersForbiddenNotNotFound(t *testing.T) {
	f := newFakeStore()
	seedEmployee(f, 2, "Someone Else", 0) // caller 5 has no employee row
	svc := newSvc(f)

	ctx := asUser(auth.RoleEmployee, map[string]bool{"hr.leaves.store": true})
	_, err := svc.CreateLeave(ctx, 5, leaveInput(2))
	if !errors.Is(err, ErrLeaveForOther) {
		t.Fatalf("err = %v, want ErrLeaveForOther (403), not ErrEmployeeRequired (404)", err)
	}
}

// A caller with no employee row may still file for someone else when they hold
// the permission — their own row is only needed to compare against.
func TestCreateLeave_NoOwnEmployeeRowCanStillFileWithPermission(t *testing.T) {
	f := newFakeStore()
	seedEmployee(f, 2, "Someone Else", 0)
	svc := newSvc(f)

	ctx := asUser(auth.RoleHRStaff, map[string]bool{LeaveTargetAnyPermission: true})
	got, err := svc.CreateLeave(ctx, 5, leaveInput(2))
	if err != nil {
		t.Fatalf("CreateLeave: %v", err)
	}
	if got.EmployeeID != 2 {
		t.Fatalf("employee_id = %d, want 2", got.EmployeeID)
	}
}

func TestCreateLeave_RejectsUnknownTargetInsteadOfFKFailure(t *testing.T) {
	f := newFakeStore()
	seedEmployee(f, 1, "HR Staff", 5)
	svc := newSvc(f)

	ctx := asUser(auth.RoleHRStaff, map[string]bool{LeaveTargetAnyPermission: true})
	_, err := svc.CreateLeave(ctx, 5, leaveInput(999))
	if !errors.Is(err, ErrNotFound) {
		t.Fatalf("err = %v, want ErrNotFound", err)
	}
}

// Self-approval: previously reviewLeave checked only that the leave was
// pending, so filing and approving in one session worked.
func TestReviewLeave_BlocksSelfApproval(t *testing.T) {
	f := newFakeStore()
	seedEmployee(f, 1, "Owner", 5)
	svc := newSvc(f)

	ctx := asUser(auth.RoleAdmin, nil)
	created, err := svc.CreateLeave(ctx, 5, leaveInput(1))
	if err != nil {
		t.Fatalf("CreateLeave: %v", err)
	}
	if _, err := svc.ApproveLeave(ctx, created.ID, 5); !errors.Is(err, ErrLeaveSelfApproval) {
		t.Fatalf("self-approval err = %v, want ErrLeaveSelfApproval", err)
	}
	if l := leaveFromRow(f.leaves[created.ID]); l.Status != "pending" {
		t.Fatalf("status = %q, want the leave left pending", l.Status)
	}
}

func TestReviewLeave_AllowsApprovingSomebodyElses(t *testing.T) {
	f := newFakeStore()
	seedEmployee(f, 1, "HR Staff", 5)
	seedEmployee(f, 2, "Staff On Leave", 7)
	svc := newSvc(f)

	ctx := asUser(auth.RoleHRStaff, map[string]bool{LeaveTargetAnyPermission: true})
	created, err := svc.CreateLeave(ctx, 5, leaveInput(2))
	if err != nil {
		t.Fatalf("CreateLeave: %v", err)
	}
	// User 7 is the leave's employee, so user 5 approving is legitimate.
	if _, err := svc.ApproveLeave(ctx, created.ID, 5); err != nil {
		t.Fatalf("ApproveLeave: %v", err)
	}
	if l := leaveFromRow(f.leaves[created.ID]); l.Status != "approved" {
		t.Fatalf("status = %q, want approved", l.Status)
	}
}

func TestReviewLeave_ApproverWithoutEmployeeRecordIsAllowed(t *testing.T) {
	// An admin with no employee row is the normal approver; there is nothing
	// to compare, so the guard must not block them.
	f := newFakeStore()
	seedEmployee(f, 2, "Staff On Leave", 7)
	svc := newSvc(f)

	ctx := asUser(auth.RoleHRStaff, map[string]bool{LeaveTargetAnyPermission: true})
	created, err := svc.CreateLeave(ctx, 5, leaveInput(2))
	if err != nil {
		t.Fatalf("CreateLeave: %v", err)
	}
	// Caller 5 has no employee row, so self-filing cannot work; go through 7.
	if _, err := svc.ApproveLeave(ctx, created.ID, 5); err != nil {
		t.Fatalf("approver without an employee row was blocked: %v", err)
	}
}