package creditlimit

import (
	"bytes"
	"errors"
	"strings"
	"testing"
	"time"

	"system-altrak/internal/domain"
	"system-altrak/internal/dto"

	"github.com/xuri/excelize/v2"
)

type creditLimitRepoMock struct {
	createErr   error
	sequence    int
	sequenceErr error
	created     *domain.CreditLimit
	updated     *domain.CreditLimit
	getByID     *domain.CreditLimit
	getByIDErr  error
	list        []domain.CreditLimit
	listErr     error
	deletedID   uint
	deletedRole string
	deletedBr   uint
	deleteErr   error
}

func (m *creditLimitRepoMock) Create(cl *domain.CreditLimit) error {
	m.created = cl
	return m.createErr
}

func (m *creditLimitRepoMock) Update(cl *domain.CreditLimit) error {
	m.updated = cl
	return nil
}

func (m *creditLimitRepoMock) GetByID(id uint, branchID uint, role string) (*domain.CreditLimit, error) {
	if m.getByIDErr != nil {
		return nil, m.getByIDErr
	}
	if m.getByID == nil {
		return nil, errors.New("not found")
	}
	copy := *m.getByID
	return &copy, nil
}

func (m *creditLimitRepoMock) List(branchID uint, role string) ([]domain.CreditLimit, error) {
	if m.listErr != nil {
		return nil, m.listErr
	}
	return m.list, nil
}

func (m *creditLimitRepoMock) GetNextSequence(docType string, year, month int) (int, error) {
	if m.sequenceErr != nil {
		return 0, m.sequenceErr
	}
	return m.sequence, nil
}

func (m *creditLimitRepoMock) Delete(id uint, branchID uint, role string) error {
	m.deletedID = id
	m.deletedBr = branchID
	m.deletedRole = role
	return m.deleteErr
}

type settingServiceMock struct {
	value   string
	err     error
	getKey  string
	setKey  string
	setVal  string
	setErr  error
	listErr error
}

func (m *settingServiceMock) Get(key string) (string, error) {
	m.getKey = key
	if m.err != nil {
		return "", m.err
	}
	return m.value, nil
}

func (m *settingServiceMock) Set(key, value string) error {
	m.setKey = key
	m.setVal = value
	return m.setErr
}

func (m *settingServiceMock) List() ([]domain.SystemSetting, error) {
	if m.listErr != nil {
		return nil, m.listErr
	}
	return nil, nil
}

func (m *settingServiceMock) ListPermissions() ([]domain.RolePermission, error) {
	return nil, nil
}

func (m *settingServiceMock) UpdatePermission(role, module string, canView, canCreate, canEdit, canDelete, canExport bool) error {
	return nil
}

func (m *settingServiceMock) GetStats() (map[string]interface{}, error) {
	return nil, nil
}

func (m *settingServiceMock) OptimizeDB() error {
	return nil
}

func openCreditLimitWorkbook(t *testing.T, raw []byte) *excelize.File {
	t.Helper()
	f, err := excelize.OpenReader(bytes.NewReader(raw))
	if err != nil {
		t.Fatalf("failed to open workbook: %v", err)
	}
	t.Cleanup(func() { _ = f.Close() })
	return f
}

func TestCreateCreditLimitUsesSettingsPatternAndRawLimit(t *testing.T) {
	repo := &creditLimitRepoMock{sequence: 17}
	set := &settingServiceMock{value: "{INC}/CL/{YY}"}
	svc := NewService(repo, nil, set)

	req := &dto.CreditLimitRequest{
		Customer:          "PT Alpha",
		CustomerCode:      "ALP-01",
		RequestedLimitRaw: "Rp 12.345.678",
		Spareparts:        100,
		FilterAmount:      50,
		Valvoline:         25,
		Others:            10,
		AgingNotDue:       40,
		Aging1Month:       10,
		Aging2Months:      5,
		Aging3Months:      1,
		AgingOver3:        2,
	}

	created, err := svc.Create(req, 99, 3)
	if err != nil {
		t.Fatalf("Create returned error: %v", err)
	}
	if repo.created == nil {
		t.Fatal("expected repo.Create to receive payload")
	}
	if set.getKey != "cl_format" {
		t.Fatalf("expected setting key cl_format, got %q", set.getKey)
	}
	if created.RequestedLimit != 12345678 {
		t.Fatalf("expected parsed requested limit 12345678, got %.0f", created.RequestedLimit)
	}
	if created.Total != 185 {
		t.Fatalf("expected total 185, got %.0f", created.Total)
	}
	if created.AgingTotal != 58 {
		t.Fatalf("expected aging total 58, got %.0f", created.AgingTotal)
	}
	if created.SignedBy != "DWI HANDOKO" || created.SignedTitle != "Branch Head" {
		t.Fatalf("expected default signature fields, got %q / %q", created.SignedBy, created.SignedTitle)
	}
	if created.Status != "draft" {
		t.Fatalf("expected status draft, got %q", created.Status)
	}
	if created.CreatedBy == nil || *created.CreatedBy != 99 {
		t.Fatal("expected CreatedBy=99")
	}
	if created.BranchID != 3 {
		t.Fatalf("expected BranchID=3, got %d", created.BranchID)
	}
	if created.Ref == "" || !strings.Contains(created.Ref, "/CL/") {
		t.Fatalf("expected generated ref contains /CL/, got %q", created.Ref)
	}
}

func TestUpdateCreditLimitPreservesRefAndRecalculatesTotals(t *testing.T) {
	original := &domain.CreditLimit{
		BaseModel:      domain.BaseModel{ID: 11},
		Ref:            "011/IOM-BJM/IV/26",
		Customer:       "PT Old",
		CustomerCode:   "OLD-1",
		RequestedLimit: 1000,
		Spareparts:     10,
		FilterAmount:   20,
		Valvoline:      30,
		Others:         40,
		AgingNotDue:    1,
		Aging1Month:    2,
		Aging2Months:   3,
		Aging3Months:   4,
		AgingOver3:     5,
		SignedBy:       "Old Signer",
		SignedTitle:    "Old Title",
		RevisionNumber: 2,
	}
	repo := &creditLimitRepoMock{getByID: original}
	svc := NewService(repo, nil, &settingServiceMock{})

	req := &dto.CreditLimitRequest{
		Customer:          "PT New",
		CustomerCode:      "NEW-9",
		RequestedLimitRaw: "Rp 9.876.543",
		Spareparts:        100,
		FilterAmount:      50,
		Valvoline:         25,
		Others:            10,
		AgingNotDue:       40,
		Aging1Month:       10,
		Aging2Months:      5,
		Aging3Months:      1,
		AgingOver3:        2,
		SignedBy:          "New Signer",
		SignedTitle:       "New Title",
	}

	updated, err := svc.Update(11, req, 3, "superadmin")
	if err != nil {
		t.Fatalf("Update returned error: %v", err)
	}
	if repo.updated == nil {
		t.Fatal("expected repo.Update to be called")
	}
	if updated.Ref != original.Ref {
		t.Fatalf("expected ref to remain unchanged, got %q", updated.Ref)
	}
	if updated.Customer != "PT New" || updated.CustomerCode != "NEW-9" {
		t.Fatalf("unexpected updated customer fields: %+v", updated)
	}
	if updated.RequestedLimit != 9876543 {
		t.Fatalf("expected parsed requested limit 9876543, got %.0f", updated.RequestedLimit)
	}
	if updated.Total != 185 || updated.AgingTotal != 58 {
		t.Fatalf("unexpected recalculated totals total=%.0f aging=%.0f", updated.Total, updated.AgingTotal)
	}
	if updated.SignedBy != "New Signer" || updated.SignedTitle != "New Title" {
		t.Fatalf("expected signature fields to update, got %q / %q", updated.SignedBy, updated.SignedTitle)
	}
	if updated.RevisionNumber != 3 {
		t.Fatalf("expected revision number to increment, got %d", updated.RevisionNumber)
	}
}

func TestCreateCreditLimitFallsBackPatternOnSettingError(t *testing.T) {
	repo := &creditLimitRepoMock{sequence: 3}
	set := &settingServiceMock{err: errors.New("missing setting")}
	svc := NewService(repo, nil, set)

	req := &dto.CreditLimitRequest{
		Customer:       "PT Beta",
		CustomerCode:   "BET-02",
		RequestedLimit: 1000,
		SignedBy:       "Custom Signer",
		SignedTitle:    "GM",
	}

	created, err := svc.Create(req, 1, 1)
	if err != nil {
		t.Fatalf("Create returned error: %v", err)
	}
	if !strings.Contains(created.Ref, "IOM-BJM") {
		t.Fatalf("expected fallback format reference containing IOM-BJM, got %q", created.Ref)
	}
	if created.SignedBy != "Custom Signer" || created.SignedTitle != "GM" {
		t.Fatalf("expected custom signature to remain, got %q / %q", created.SignedBy, created.SignedTitle)
	}
}

func TestCreateCreditLimitReturnsSequenceError(t *testing.T) {
	repo := &creditLimitRepoMock{sequenceErr: errors.New("seq failed")}
	set := &settingServiceMock{value: "{INC}/CL/{YY}"}
	svc := NewService(repo, nil, set)

	_, err := svc.Create(&dto.CreditLimitRequest{Customer: "C", CustomerCode: "C1"}, 1, 1)
	if err == nil {
		t.Fatal("expected sequence error")
	}
	if !strings.Contains(err.Error(), "failed to get sequence") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestExportExcelCreditLimitProducesExpectedSheet(t *testing.T) {
	repo := &creditLimitRepoMock{
		list: []domain.CreditLimit{
			{
				Ref:            "001/CL/26",
				Date:           time.Date(2026, time.April, 13, 0, 0, 0, 0, time.UTC),
				Customer:       "PT Gamma",
				CustomerCode:   "GAM-03",
				RequestedLimit: 2000,
				Total:          700,
				AgingTotal:     100,
				Status:         "draft",
			},
		},
	}
	svc := NewService(repo, nil, &settingServiceMock{})

	raw, err := svc.ExportExcel(1, "admin")
	if err != nil {
		t.Fatalf("ExportExcel returned error: %v", err)
	}

	book := openCreditLimitWorkbook(t, raw)

	h, _ := book.GetCellValue("Credit Limit IOM", "A1")
	if h != "No Dokumen" {
		t.Fatalf("expected header No Dokumen at A1, got %q", h)
	}
	ref, _ := book.GetCellValue("Credit Limit IOM", "A2")
	if ref != "001/CL/26" {
		t.Fatalf("expected ref row value, got %q", ref)
	}
	customer, _ := book.GetCellValue("Credit Limit IOM", "C2")
	if customer != "PT Gamma" {
		t.Fatalf("expected customer PT Gamma, got %q", customer)
	}
}

func TestDeleteCreditLimitDelegates(t *testing.T) {
	repo := &creditLimitRepoMock{}
	svc := NewService(repo, nil, &settingServiceMock{})

	if err := svc.Delete(7, 2, "admin"); err != nil {
		t.Fatalf("Delete returned error: %v", err)
	}
	if repo.deletedID != 7 || repo.deletedBr != 2 || repo.deletedRole != "admin" {
		t.Fatalf("unexpected delete delegation: id=%d branch=%d role=%q", repo.deletedID, repo.deletedBr, repo.deletedRole)
	}
}

func TestFormatRupiah(t *testing.T) {
	if got := formatRupiah(0); got != "0" {
		t.Fatalf("expected 0, got %q", got)
	}
	if got := formatRupiah(1234567.4); got != "1.234.567" {
		t.Fatalf("expected 1.234.567, got %q", got)
	}
	if got := formatRupiah(-2500.8); got != "-2.501" {
		t.Fatalf("expected -2.501, got %q", got)
	}
}
