package pdks

import (
	"context"
	"testing"
	"time"
)

// sabitSaat, testlerde gunluk saat uretir (2026-10-05 = Pazartesi).
func testClock() func() time.Time {
	return func() time.Time {
		return time.Date(2026, 10, 5, 18, 0, 0, 0, time.Local)
	}
}

func at(h, m int) time.Time {
	return time.Date(2026, 10, 5, h, m, 0, 0, time.Local)
}

func newSvc(t *testing.T) (*Service, *MemoryRepo) {
	t.Helper()
	repo := NewMemoryRepo()
	repo.SetNow(testClock())
	return New(Options{Repo: repo, Now: testClock()}), repo
}

func mustEmp(t *testing.T, s *Service, name string) Employee {
	t.Helper()
	e, err := s.CreateEmployee(context.Background(), Employee{Name: name})
	if err != nil {
		t.Fatalf("personel: %v", err)
	}
	return e
}

// TestPunch_GirisCikisSuresi temel senaryo: 09:00 giris, 18:00 cikis = 9 saat.
func TestPunch_GirisCikisSuresi(t *testing.T) {
	ctx := context.Background()
	s, _ := newSvc(t)
	emp := mustEmp(t, s, "Ali")

	if _, err := s.Punch(ctx, Punch{EmpID: emp.ID, Type: AttendanceIn, At: at(9, 0)}); err != nil {
		t.Fatal(err)
	}
	if _, err := s.Punch(ctx, Punch{EmpID: emp.ID, Type: AttendanceOut, At: at(18, 0)}); err != nil {
		t.Fatal(err)
	}
	sum, err := s.Day(ctx, emp.ID, "2026-10-05")
	if err != nil {
		t.Fatal(err)
	}
	if sum.WorkMin != 540 {
		t.Errorf("WorkMin = %d, beklenen 540 (9 saat)", sum.WorkMin)
	}
	if sum.Status != StatusPresent {
		t.Errorf("Status = %q, beklenen present", sum.Status)
	}
	if sum.FirstIn != "09:00" || sum.LastOut != "18:00" {
		t.Errorf("FirstIn/LastOut = %q/%q", sum.FirstIn, sum.LastOut)
	}
}

// TestPunch_MolaDusulur 09:00-18:00 arasi 1 saat mola = 8 saat NET calisma.
func TestPunch_MolaDusulur(t *testing.T) {
	ctx := context.Background()
	s, _ := newSvc(t)
	emp := mustEmp(t, s, "Veli")

	must := func(ty AttendanceType, h, m int) {
		t.Helper()
		if _, err := s.Punch(ctx, Punch{EmpID: emp.ID, Type: ty, At: at(h, m)}); err != nil {
			t.Fatalf("%s: %v", ty, err)
		}
	}
	must(AttendanceIn, 9, 0)
	must(AttendanceBreakStart, 12, 0)
	must(AttendanceBreakEnd, 13, 0)
	must(AttendanceOut, 18, 0)

	sum, _ := s.Day(ctx, emp.ID, "2026-10-05")
	if sum.WorkMin != 480 {
		t.Errorf("WorkMin = %d, beklenen 480 (9 saat - 1 saat mola)", sum.WorkMin)
	}
	if sum.BreakMin != 60 {
		t.Errorf("BreakMin = %d, beklenen 60", sum.BreakMin)
	}
}

// TestPunch_CiftGirisEngellenir: ayni gun iki giris SESSIZCE kabul edilmez
// (sure hesabi bozulurdu).
func TestPunch_CiftGirisEngellenir(t *testing.T) {
	ctx := context.Background()
	s, _ := newSvc(t)
	emp := mustEmp(t, s, "Ayse")

	if _, err := s.Punch(ctx, Punch{EmpID: emp.ID, Type: AttendanceIn, At: at(9, 0)}); err != nil {
		t.Fatal(err)
	}
	_, err := s.Punch(ctx, Punch{EmpID: emp.ID, Type: AttendanceIn, At: at(9, 30)})
	if err == nil {
		t.Fatal("cift giris kabul edilmemeli")
	}
}

// TestPunch_CikissizGirisHatasi: giris yoksa cikis reddedilir.
func TestPunch_CikissizGirisHatasi(t *testing.T) {
	ctx := context.Background()
	s, _ := newSvc(t)
	emp := mustEmp(t, s, "Can")

	if _, err := s.Punch(ctx, Punch{EmpID: emp.ID, Type: AttendanceOut, At: at(18, 0)}); err == nil {
		t.Fatal("giris yokken cikis kabul edilmemeli")
	}
}

// TestPunch_GunHalenSuryorkenAcik hesaplanir: cikis yapilmamis ise "open".
func TestPunch_GunHalenSuryorkenAcik(t *testing.T) {
	ctx := context.Background()
	s, _ := newSvc(t)
	emp := mustEmp(t, s, "Deniz")

	if _, err := s.Punch(ctx, Punch{EmpID: emp.ID, Type: AttendanceIn, At: at(9, 0)}); err != nil {
		t.Fatal(err)
	}
	// Saat kaynagi 18:00 -> acik giris 9 saat sayilir.
	sum, _ := s.Day(ctx, emp.ID, "2026-10-05")
	if sum.Status != StatusOpen {
		t.Errorf("Status = %q, beklenen open", sum.Status)
	}
	if sum.WorkMin != 540 {
		t.Errorf("WorkMin = %d, beklenen 540 (18:00-09:00)", sum.WorkMin)
	}
}

// TestPunch_OgleyeGiripDonus: arada cikis+tekrar giris (moladan sonra devam).
func TestPunch_OgleyeGiripDonus(t *testing.T) {
	ctx := context.Background()
	s, _ := newSvc(t)
	emp := mustEmp(t, s, "Ece")

	seq := []struct {
		ty   AttendanceType
		h, m int
	}{
		{AttendanceIn, 9, 0},
		{AttendanceOut, 12, 0},
		{AttendanceIn, 13, 0},
		{AttendanceOut, 18, 0},
	}
	for _, x := range seq {
		if _, err := s.Punch(ctx, Punch{EmpID: emp.ID, Type: x.ty, At: at(x.h, x.m)}); err != nil {
			t.Fatalf("%s: %v", x.ty, err)
		}
	}
	sum, _ := s.Day(ctx, emp.ID, "2026-10-05")
	// 09-12 (3h) + 13-18 (5h) = 8 saat
	if sum.WorkMin != 480 {
		t.Errorf("WorkMin = %d, beklenen 480", sum.WorkMin)
	}
}

// TestDay_GecKalma: vardiya 09:00-18:00, giris 09:20 -> 20 dk gec kalma.
func TestDay_GecKalma(t *testing.T) {
	ctx := context.Background()
	s, _ := newSvc(t)
	emp := mustEmp(t, s, "Gec")
	if _, err := s.CreateShift(ctx, Shift{Name: "Normal", Start: "09:00", End: "18:00", BreakMin: 60, Active: true}); err != nil {
		t.Fatal(err)
	}
	must := func(ty AttendanceType, h, m int) {
		t.Helper()
		if _, err := s.Punch(ctx, Punch{EmpID: emp.ID, Type: ty, At: at(h, m)}); err != nil {
			t.Fatal(err)
		}
	}
	must(AttendanceIn, 9, 20)
	must(AttendanceOut, 18, 0)

	sum, _ := s.Day(ctx, emp.ID, "2026-10-05")
	if sum.LateMin != 20 {
		t.Errorf("LateMin = %d, beklenen 20", sum.LateMin)
	}
	if sum.OvertimeMin != 0 {
		t.Errorf("OvertimeMin = %d, beklenen 0", sum.OvertimeMin)
	}
}

// TestDay_Mesai: vardiya 8 saat (09-18, 1 saat mola), calisma 10 saat -> 2 saat mesai.
func TestDay_Mesai(t *testing.T) {
	ctx := context.Background()
	s, _ := newSvc(t)
	emp := mustEmp(t, s, "Mesai")
	if _, err := s.CreateShift(ctx, Shift{Name: "Normal", Start: "09:00", End: "18:00", BreakMin: 60, Active: true}); err != nil {
		t.Fatal(err)
	}
	must := func(ty AttendanceType, h, m int) {
		t.Helper()
		if _, err := s.Punch(ctx, Punch{EmpID: emp.ID, Type: ty, At: at(h, m)}); err != nil {
			t.Fatal(err)
		}
	}
	must(AttendanceIn, 9, 0)
	must(AttendanceOut, 20, 0)

	sum, _ := s.Day(ctx, emp.ID, "2026-10-05")
	// 09:00-20:00 arasi molasiz calisma = 11 saat. Vardiya plani 8 saat net
	// (09-18 arasi 9 saat eksi 60 dk planli mola). Mesai = 11 - 8 = 3 saat.
	// NOT: kullanici molasiz calistigi icin net calisma 11 saattir.
	if sum.OvertimeMin != 180 {
		t.Errorf("OvertimeMin = %d, beklenen 180 (3 saat)", sum.OvertimeMin)
	}
}

// TestDay_GidisizDevamsiz: hic kayit yok -> absent.
func TestDay_GidisizDevamsiz(t *testing.T) {
	ctx := context.Background()
	s, _ := newSvc(t)
	emp := mustEmp(t, s, "Gelmedi")
	sum, _ := s.Day(ctx, emp.ID, "2026-10-05")
	if sum.Status != StatusAbsent {
		t.Errorf("Status = %q, beklenen absent", sum.Status)
	}
	if sum.WorkMin != 0 {
		t.Errorf("WorkMin = %d, beklenen 0", sum.WorkMin)
	}
}

// TestLeave_GunIzinliSayilir onayli izin gunu "leave" olur, calisma sayilmaz.
func TestLeave_GunIzinliSayilir(t *testing.T) {
	ctx := context.Background()
	s, _ := newSvc(t)
	emp := mustEmp(t, s, "Izinli")
	if _, err := s.CreateLeave(ctx, Leave{
		EmpID: emp.ID, Kind: LeaveAnnual,
		From: "2026-10-05", To: "2026-10-07", Approved: true,
	}); err != nil {
		t.Fatal(err)
	}
	sum, _ := s.Day(ctx, emp.ID, "2026-10-05")
	if sum.Status != StatusLeave {
		t.Errorf("Status = %q, beklenen leave", sum.Status)
	}
}

// TestReport_AralikToplamlari 3 gunluk raporun toplamlarini dogrular.
// TestReport_AralikToplamlari 3 gunluk raporun toplamlarini dogrular.
func TestReport_AralikToplamlari(t *testing.T) {
	ctx := context.Background()
	s, _ := newSvc(t)
	emp := mustEmp(t, s, "Rapor")
	// Her gun: giris 09:00, belirtilen saatte cikis (gun tamamlanmis).
	for _, x := range []struct {
		d    string
		h, m int
	}{
		{"2026-10-05", 17, 0}, // 8 saat
		{"2026-10-06", 15, 0}, // 6 saat
	} {
		day, err := ParseDayOf(x.d)
		if err != nil {
			t.Fatal(err)
		}
		in := time.Date(day.Year(), day.Month(), day.Day(), 9, 0, 0, 0, time.Local)
		outAt := time.Date(day.Year(), day.Month(), day.Day(), x.h, x.m, 0, 0, time.Local)
		if _, err := s.Punch(ctx, Punch{EmpID: emp.ID, Type: AttendanceIn, At: in}); err != nil {
			t.Fatalf("%s in: %v", x.d, err)
		}
		if _, err := s.Punch(ctx, Punch{EmpID: emp.ID, Type: AttendanceOut, At: outAt}); err != nil {
			t.Fatalf("%s out: %v", x.d, err)
		}
	}
	rep, err := s.Report(ctx, emp.ID, "2026-10-05", "2026-10-07")
	if err != nil {
		t.Fatal(err)
	}
	if rep.Days != 3 {
		t.Errorf("Days = %d, beklenen 3", rep.Days)
	}
	if len(rep.Rows) != 1 {
		t.Fatalf("Rows = %d, beklenen 1", len(rep.Rows))
	}
	// 5 ve 6 tam kapali -> present; 7 kayitsiz -> absent.
	if rep.Rows[0].Present != 2 {
		t.Errorf("Present = %d, beklenen 2", rep.Rows[0].Present)
	}
	if rep.Rows[0].Absent != 1 {
		t.Errorf("Absent = %d, beklenen 1", rep.Rows[0].Absent)
	}
	// 8 saat + 6 saat = 14 saat = 840 dk
	if rep.Rows[0].WorkMin != 840 {
		t.Errorf("WorkMin = %d, beklenen 840", rep.Rows[0].WorkMin)
	}
	if rep.WorkMin != 840 {
		t.Errorf("rapor WorkMin = %d, beklenen 840", rep.WorkMin)
	}
}

// TestCreateDepartment_DonguEngelli bolum agacinda dongu kurulamaz.
func TestCreateDepartment_DonguEngelli(t *testing.T) {
	ctx := context.Background()
	s, _ := newSvc(t)
	root, err := s.CreateDepartment(ctx, Department{ID: "d1", Name: "Kok"})
	if err != nil {
		t.Fatal(err)
	}
	child, err := s.CreateDepartment(ctx, Department{ID: "d2", Name: "Alt", ParentID: root.ID})
	if err != nil {
		t.Fatal(err)
	}
	// d1 -> d2 -> d1 dongusu
	if _, err := s.UpdateDepartment(ctx, Department{ID: "d1", Name: "Kok", ParentID: child.ID}); err == nil {
		t.Fatal("bolum agacinda dongu engellenmeli")
	}
}

// TestPunch_CikisSonrasiTekrarGiris gun icinde birden fazla giris/cikis dongusunu
// dener (ogle arasi cikis, tekrar giris). Bu CANLI HATA olarak bulundu: once
// giris -> cikis -> tekrar giris denemesi 409 donuyordu.
func TestPunch_CikisSonrasiTekrarGiris(t *testing.T) {
	ctx := context.Background()
	s, _ := newSvc(t)
	emp := mustEmp(t, s, "Tekrar")

	seq := []struct {
		ty   AttendanceType
		h, m int
	}{
		{AttendanceIn, 9, 0},
		{AttendanceOut, 12, 0},
		{AttendanceIn, 13, 0}, // tekrar giris: ACIK OLMALI
		{AttendanceOut, 18, 0},
	}
	for i, x := range seq {
		if _, err := s.Punch(ctx, Punch{EmpID: emp.ID, Type: x.ty, At: at(x.h, x.m)}); err != nil {
			t.Fatalf("%d. adim (%s %02d:%02d) reddedildi: %v", i+1, x.ty, x.h, x.m, err)
		}
	}
	sum, _ := s.Day(ctx, emp.ID, "2026-10-05")
	if sum.WorkMin != 480 {
		t.Errorf("WorkMin = %d, beklenen 480", sum.WorkMin)
	}
}
