package salestracking

import (
	"context"
	"errors"
	"testing"
	"time"

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

type fakeStore struct {
	visits map[int64]db.GetSalesVisitByIDRow
	routes map[int64]db.ListSalesRoutesRow
	locs   []db.CreateSalesLocationParams
	nextV  int64
	nextR  int64
	nextA  int64
	custOK bool
}

func newFakeStore() *fakeStore {
	return &fakeStore{visits: map[int64]db.GetSalesVisitByIDRow{}, routes: map[int64]db.ListSalesRoutesRow{}, custOK: true, nextV: 1, nextR: 1, nextA: 1}
}

func (f *fakeStore) ListVisits(context.Context, Filter, *int64) ([]db.ListSalesVisitsRow, int64, error) {
	return nil, 0, nil
}
func (f *fakeStore) ListActivities(_ context.Context, _ Filter, _ *int64) ([]db.ListSalesActivitiesRow, int64, error) {
	return nil, 0, nil
}
func (f *fakeStore) VisitByID(_ context.Context, id int64) (db.GetSalesVisitByIDRow, error) {
	if v, ok := f.visits[id]; ok {
		return v, nil
	}
	return db.GetSalesVisitByIDRow{}, ErrNotFound
}
func (f *fakeStore) CreateVisit(_ context.Context, p db.CreateSalesVisitParams) (int64, error) {
	id := f.nextV
	f.nextV++
	f.visits[id] = db.GetSalesVisitByIDRow{
		ID:          id,
		SalesID:     p.SalesID,
		CustomerID:  p.CustomerID,
		VisitDate:   p.VisitDate,
		Purpose:     p.Purpose,
		Notes:       p.Notes,
		Status:      p.Status,
		VisitType:   p.VisitType,
		ScheduledBy: p.ScheduledBy,
	}
	return id, nil
}
func (f *fakeStore) UpdateVisit(_ context.Context, p db.UpdateSalesVisitParams) error {
	v, ok := f.visits[p.ID]
	if !ok {
		return ErrNotFound
	}
	v.CheckInTime = p.CheckInTime
	v.CheckOutTime = p.CheckOutTime
	v.CheckInLat = p.CheckInLat
	v.CheckInLng = p.CheckInLng
	v.CheckOutLat = p.CheckOutLat
	v.CheckOutLng = p.CheckOutLng
	v.Result = p.Result
	v.Notes = p.Notes
	v.Status = p.Status
	f.visits[p.ID] = v
	return nil
}
func (f *fakeStore) UpdateVisitPhotos(_ context.Context, _ int64, _, _ *string) error { return nil }
func (f *fakeStore) CustomerExists(context.Context, int64) (bool, error) { return f.custOK, nil }

func (f *fakeStore) CreateActivity(context.Context, db.CreateSalesActivityParams) (int64, error) {
	id := f.nextA
	f.nextA++
	return id, nil
}

func (f *fakeStore) ListRoutes(context.Context, Filter, *int64) ([]db.ListSalesRoutesRow, int64, error) {
	return nil, 0, nil
}
func (f *fakeStore) CreateRoute(_ context.Context, p db.CreateSalesRouteParams) (int64, error) {
	id := f.nextR
	f.nextR++
	f.routes[id] = db.ListSalesRoutesRow{
		ID:               id,
		SalesID:          p.SalesID,
		RouteDate:        p.RouteDate,
		Name:             p.Name,
		PlannedCustomers: p.PlannedCustomers,
		Status:           p.Status,
		Notes:            p.Notes,
	}
	return id, nil
}

func (f *fakeStore) InsertLocations(_ context.Context, locs []db.CreateSalesLocationParams) error {
	f.locs = append(f.locs, locs...)
	return nil
}
func (f *fakeStore) LatestLocations(context.Context) ([]db.LatestSalesLocationsRow, error) {
	return nil, nil
}

func (f *fakeStore) ListCustomerLocations(context.Context, string, string, int, int) ([]CustomerLocation, int64, error) {
	return nil, 0, nil
}

func (f *fakeStore) ListSalesEmployees(context.Context) ([]db.ListSalesEmployeesRow, error) {
	return nil, nil
}

func newSvc(s *fakeStore) *Service {
	return NewService(s, time.FixedZone("WIB", 7*3600)).WithEmployeeLookup(
		func(_ context.Context, userID int64) (int64, error) {
			if userID == 0 {
				return 0, ErrNoEmployee
			}
			return userID + 10, nil
		})
}

func TestCreateVisit_StatusPlanned(t *testing.T) {
	s := newFakeStore()
	svc := newSvc(s)
	v, err := svc.CreateVisit(context.Background(), 1, VisitInput{CustomerID: 5, VisitDate: "2026-08-11", Purpose: "Follow up"}, SchedulePermission{})
	if err != nil {
		t.Fatal(err)
	}
	if v.Status != "planned" {
		t.Errorf("status = %q, want planned", v.Status)
	}
	if v.SalesID != 11 {
		t.Errorf("sales_id = %d, want 11 (user 1)", v.SalesID)
	}
}

func TestCreateVisit_RequiresCustomer(t *testing.T) {
	s := newFakeStore()
	s.custOK = false
	svc := newSvc(s)
	if _, err := svc.CreateVisit(context.Background(), 1, VisitInput{CustomerID: 5, VisitDate: "2026-08-11", Purpose: "x"}, SchedulePermission{}); !errors.Is(err, ErrCustomerReq) {
		t.Fatalf("got %v, want ErrCustomerReq", err)
	}
}

func TestCreateVisit_NoEmployee(t *testing.T) {
	svc := NewService(newFakeStore(), time.UTC)
	if _, err := svc.CreateVisit(context.Background(), 1, VisitInput{CustomerID: 5, VisitDate: "2026-08-11", Purpose: "x"}, SchedulePermission{}); !errors.Is(err, ErrNoEmployee) {
		t.Fatalf("got %v, want ErrNoEmployee", err)
	}
}

// The 2 Nov flow audit: a back-office scheduler (admin, or an SPV user
// without an employees row) got 404 "Not found" on every save because the
// caller's employee lookup ran before anything else. Scheduling FOR a sales
// rep must succeed with scheduled_by NULL — only a self-schedule needs the
// caller's own employees row.
func TestCreateVisit_SchedulerWithoutEmployeeRow(t *testing.T) {
	s := newFakeStore()
	svc := NewService(s, time.UTC) // no employee lookup: caller has none
	salesID := int64(1)
	v, err := svc.CreateVisit(context.Background(), 42,
		VisitInput{SalesID: &salesID, CustomerID: 5, VisitDate: "2026-11-02", Purpose: "kunjungan rutin"},
		SchedulePermission{CanRoutine: true})
	if err != nil {
		t.Fatalf("schedule-for-other without an employees row: %v", err)
	}
	if v.SalesID != 1 {
		t.Errorf("sales_id = %d, want the target sales 1", v.SalesID)
	}
	if v.ScheduledBy != nil {
		t.Errorf("scheduled_by = %v, want NULL (scheduler has no employees row)", *v.ScheduledBy)
	}
}

func TestCreateVisit_SelfScheduleWithoutEmployeeRowRefused(t *testing.T) {
	svc := NewService(newFakeStore(), time.UTC)
	// No SalesID → the visit would anchor to the caller, which has no
	// employees row: refused, and through the handler it reads as a clear 422,
	// not the old generic 404.
	if _, err := svc.CreateVisit(context.Background(), 42,
		VisitInput{CustomerID: 5, VisitDate: "2026-11-02", Purpose: "x"}, SchedulePermission{CanRoutine: true},
	); !errors.Is(err, ErrNoEmployee) {
		t.Fatalf("got %v, want ErrNoEmployee", err)
	}
}

func TestCreateVisit_ZeroSalesIDMeansSelf(t *testing.T) {
	// An unset picker that sent 0 must not be treated as "schedule for user 0".
	s := newFakeStore()
	svc := NewService(s, time.UTC)
	zero := int64(0)
	if _, err := svc.CreateVisit(context.Background(), 42,
		VisitInput{SalesID: &zero, CustomerID: 5, VisitDate: "2026-11-02", Purpose: "x"},
		SchedulePermission{CanRoutine: true},
	); !errors.Is(err, ErrNoEmployee) {
		t.Fatalf("got %v, want ErrNoEmployee (0 = self, caller has no employees row)", err)
	}
}

func TestVisit_CheckinCheckout(t *testing.T) {
	s := newFakeStore()
	svc := newSvc(s)
	v, err := svc.CreateVisit(context.Background(), 1, VisitInput{CustomerID: 5, VisitDate: "2026-08-11", Purpose: "x"}, SchedulePermission{})
	if err != nil {
		t.Fatal(err)
	}
	got, err := svc.Checkin(context.Background(), 1, v.ID, GeoInput{PhotoURL: "/uploads/checkin.jpg"})
	if err != nil {
		t.Fatal(err)
	}
	if got.Status != "in_progress" || got.CheckInTime == nil {
		t.Errorf("checkin failed: %+v", got)
	}
	if got.PhotoCheckIn == nil || *got.PhotoCheckIn != "/uploads/checkin.jpg" {
		t.Errorf("expected photo_check_in /uploads/checkin.jpg, got %v", got.PhotoCheckIn)
	}
	got, err = svc.Checkout(context.Background(), 1, v.ID, GeoInput{PhotoURL: "/uploads/checkout.jpg"})
	if err != nil {
		t.Fatal(err)
	}
	if got.Status != "completed" || got.CheckOutTime == nil {
		t.Errorf("checkout failed: %+v", got)
	}
	if got.PhotoCheckOut == nil || *got.PhotoCheckOut != "/uploads/checkout.jpg" {
		t.Errorf("expected photo_check_out /uploads/checkout.jpg, got %v", got.PhotoCheckOut)
	}
}

func TestVisit_Ownership(t *testing.T) {
	s := newFakeStore()
	svc := newSvc(s)
	v, _ := svc.CreateVisit(context.Background(), 1, VisitInput{CustomerID: 5, VisitDate: "2026-08-11", Purpose: "x"}, SchedulePermission{})
	if _, err := svc.Checkin(context.Background(), 2, v.ID, GeoInput{}); !errors.Is(err, ErrNotOwner) {
		t.Fatalf("got %v, want ErrNotOwner", err)
	}
}

func TestVisit_ManagerCheckinCheckout(t *testing.T) {
	s := newFakeStore()
	svc := newSvc(s)
	// Visit belongs to sales_id 11 (user 1).
	v, err := svc.CreateVisit(context.Background(), 1, VisitInput{CustomerID: 5, VisitDate: "2026-08-11", Purpose: "manager test"}, SchedulePermission{})
	if err != nil {
		t.Fatal(err)
	}
	// Manager user 2 (who is not owner) can check in when isManager=true.
	got, err := svc.Checkin(context.Background(), 2, v.ID, GeoInput{Notes: "Checked in by manager"}, true)
	if err != nil {
		t.Fatalf("manager checkin failed: %v", err)
	}
	if got.Status != "in_progress" || got.CheckInTime == nil {
		t.Errorf("expected in_progress, got %+v", got)
	}
	// Manager user 2 can also checkout when isManager=true.
	res, err := svc.Checkout(context.Background(), 2, v.ID, GeoInput{Notes: "Done", Result: "Sales order placed"}, true)
	if err != nil {
		t.Fatalf("manager checkout failed: %v", err)
	}
	if res.Status != "completed" || res.CheckOutTime == nil || res.Result != "Sales order placed" {
		t.Errorf("expected completed with result, got %+v", res)
	}
}

func TestVisit_CheckoutWithoutCheckin(t *testing.T) {
	s := newFakeStore()
	svc := newSvc(s)
	v, _ := svc.CreateVisit(context.Background(), 1, VisitInput{CustomerID: 5, VisitDate: "2026-08-11", Purpose: "x"}, SchedulePermission{})
	if _, err := svc.Checkout(context.Background(), 1, v.ID, GeoInput{}); !errors.Is(err, ErrVisitInvalid) {
		t.Fatalf("got %v, want ErrVisitInvalid", err)
	}
}

func TestCreateVisit_BillingRequiresBillingPermission(t *testing.T) {
	svc := newSvc(newFakeStore())
	in := VisitInput{CustomerID: 5, VisitDate: "2026-08-11", Purpose: "Penagihan", VisitType: "billing"}
	if _, err := svc.CreateVisit(context.Background(), 1, in, SchedulePermission{CanRoutine: true}); !errors.Is(err, ErrScheduleForbidden) {
		t.Fatalf("got %v, want ErrScheduleForbidden (routine perm does not grant billing)", err)
	}
	v, err := svc.CreateVisit(context.Background(), 1, in, SchedulePermission{CanBilling: true})
	if err != nil {
		t.Fatal(err)
	}
	if v.VisitType != "billing" {
		t.Errorf("visit_type = %q, want billing", v.VisitType)
	}
}

func TestCreateVisit_AssigningOtherSalesRequiresSchedulingRight(t *testing.T) {
	svc := newSvc(newFakeStore())
	target := int64(99)
	in := VisitInput{SalesID: &target, CustomerID: 5, VisitDate: "2026-08-11", Purpose: "Kunjungan rutin"}
	if _, err := svc.CreateVisit(context.Background(), 1, in, SchedulePermission{}); !errors.Is(err, ErrScheduleForbidden) {
		t.Fatalf("got %v, want ErrScheduleForbidden (plain caller cannot schedule for someone else)", err)
	}
	v, err := svc.CreateVisit(context.Background(), 1, in, SchedulePermission{CanRoutine: true})
	if err != nil {
		t.Fatal(err)
	}
	if v.SalesID != target {
		t.Errorf("sales_id = %d, want %d (assigned target)", v.SalesID, target)
	}
	if v.ScheduledBy == nil || *v.ScheduledBy != 11 {
		t.Errorf("scheduled_by = %v, want 11 (caller's employee id)", v.ScheduledBy)
	}
}

func TestCreateActivity_Validation(t *testing.T) {
	svc := newSvc(newFakeStore())
	if _, err := svc.CreateActivity(context.Background(), 1, ActivityInput{}); err == nil {
		t.Fatal("want error for missing type/date")
	}
	a, err := svc.CreateActivity(context.Background(), 1, ActivityInput{ActivityDate: "2026-08-11", ActivityType: "meeting", Description: "d"})
	if err != nil {
		t.Fatal(err)
	}
	if a.SalesID != 11 {
		t.Errorf("sales_id = %d, want 11", a.SalesID)
	}
}

func TestCreateRoute_RequiresPlannedCustomers(t *testing.T) {
	svc := newSvc(newFakeStore())
	if _, err := svc.CreateRoute(context.Background(), 1, RouteInput{Name: "Rute A"}); err == nil {
		t.Fatal("want error for empty planned_customers")
	}
	rt, err := svc.CreateRoute(context.Background(), 1, RouteInput{
		Name:             "Rute A",
		PlannedCustomers: PlannedCustomers{{CustomerID: 3, Order: 1}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if rt.Status != "planned" || len(rt.PlannedCustomers) != 1 {
		t.Errorf("route not created correctly: %+v", rt)
	}
}

func TestPingLocations_DefaultRecordedAt(t *testing.T) {
	s := newFakeStore()
	svc := newSvc(s)
	if err := svc.PingLocations(context.Background(), 1, []LocationPingInput{
		{Latitude: -6.2, Longitude: 106.8},
	}); err != nil {
		t.Fatal(err)
	}
	if len(s.locs) != 1 {
		t.Fatalf("len = %d, want 1", len(s.locs))
	}
	l := s.locs[0]
	if !l.RecordedAt.Valid {
		t.Fatal("recorded_at not defaulted")
	}
	if l.SalesID.Int64 != 11 || !l.Latitude.Valid {
		t.Errorf("location not bound: %+v", l)
	}
}
