package pso

import (
	"bytes"
	"errors"
	"io"
	"os"
	"strings"
	"testing"
	"time"

	"system-altrak/internal/domain"
	"system-altrak/pkg/utils"

	"github.com/xuri/excelize/v2"
	"gorm.io/gorm"
)

type psoRepoMock struct {
	listRecords         []domain.OperationalRecord
	listTotal           int64
	listErr             error
	getRecordByPsoNo    map[string]*domain.OperationalRecord
	getRecordByPsoNoErr error
	getRecord           *domain.OperationalRecord
	getRecordErr        error
	updatedRecord       *domain.OperationalRecord
	updateErr           error
	upsertedRecords     []*domain.OperationalRecord
	upsertErr           error
	processHandler      func(search string, branchID uint, batchSize int, process func([]domain.OperationalRecord) error) error
}

func (m *psoRepoMock) ListOutstandingRecords(page, limit int, search string, branchID uint, startDate, endDate, aging string) ([]domain.OperationalRecord, int64, error) {
	if m.listErr != nil {
		return nil, 0, m.listErr
	}

	if m.listRecords == nil {
		return nil, 0, nil
	}

	result := make([]domain.OperationalRecord, len(m.listRecords))
	copy(result, m.listRecords)
	if m.listTotal > 0 {
		return result, m.listTotal, nil
	}
	return result, int64(len(result)), nil
}

func (m *psoRepoMock) ProcessOutstandingRecordsBatches(search string, branchID uint, batchSize int, process func([]domain.OperationalRecord) error) error {
	if m.processHandler != nil {
		return m.processHandler(search, branchID, batchSize, process)
	}
	return nil
}

func (m *psoRepoMock) GetRecordByID(id uint, branchID uint) (*domain.OperationalRecord, error) {
	if m.getRecordErr != nil {
		return nil, m.getRecordErr
	}
	if m.getRecord == nil {
		return nil, nil
	}
	rec := *m.getRecord
	return &rec, nil
}

func (m *psoRepoMock) GetRecordByPsoNo(psoNo string) (*domain.OperationalRecord, error) {
	if m.getRecordByPsoNoErr != nil {
		return nil, m.getRecordByPsoNoErr
	}
	if m.getRecordByPsoNo != nil {
		if rec, ok := m.getRecordByPsoNo[psoNo]; ok && rec != nil {
			copyRec := *rec
			return &copyRec, nil
		}
	}
	return nil, gorm.ErrRecordNotFound
}

func (m *psoRepoMock) UpdateRecord(record *domain.OperationalRecord) error {
	m.updatedRecord = record
	return m.updateErr
}

func (m *psoRepoMock) VerifyRecord(id uint, branchID uint, verifiedBy uint) error {
	return nil
}

func (m *psoRepoMock) UpsertRecord(record *domain.OperationalRecord) error {
	if record != nil {
		copyRec := *record
		m.upsertedRecords = append(m.upsertedRecords, &copyRec)
	}
	return m.upsertErr
}

func (m *psoRepoMock) DeleteRecord(id uint, branchID uint) error {
	return nil
}

func (m *psoRepoMock) MarkAsExported(ids []uint, branchID uint, exportedBy uint) error {
	return nil
}

func (m *psoRepoMock) GetRecordsByPsoNos(psoNos []string) ([]domain.OperationalRecord, error) {
	var results []domain.OperationalRecord
	if m.getRecordByPsoNo != nil {
		for _, psoNo := range psoNos {
			if rec, ok := m.getRecordByPsoNo[psoNo]; ok && rec != nil {
				results = append(results, *rec)
			}
		}
	}
	return results, m.getRecordByPsoNoErr
}

func (m *psoRepoMock) WithTransaction(fn func(repo PSORepository) error) error {
	return fn(m)
}

func openWorkbookFromBytes(t *testing.T, b []byte) *excelize.File {
	t.Helper()

	f, err := excelize.OpenReader(bytes.NewReader(b))
	if err != nil {
		t.Fatalf("failed to open workbook: %v", err)
	}

	t.Cleanup(func() {
		_ = f.Close()
	})

	return f
}

func TestValidateExportSearch(t *testing.T) {
	if err := validateExportSearch(strings.Repeat("a", 255)); err != nil {
		t.Fatalf("expected 255-char search to pass, got error: %v", err)
	}

	if err := validateExportSearch(strings.Repeat("a", 256)); !errors.Is(err, ErrInvalidExportSearch) {
		t.Fatalf("expected ErrInvalidExportSearch, got: %v", err)
	}
}

func TestNormalizeCutoffDate(t *testing.T) {
	got, err := normalizeCutoffDate("2026-04-13")
	if err != nil {
		t.Fatalf("normalizeCutoffDate returned error: %v", err)
	}
	if got != "13/04/2026" {
		t.Fatalf("expected 13/04/2026, got %s", got)
	}

	if _, err := normalizeCutoffDate("13-04-2026"); !errors.Is(err, ErrInvalidCutoffDate) {
		t.Fatalf("expected ErrInvalidCutoffDate, got: %v", err)
	}

	fallback, err := normalizeCutoffDate("")
	if err != nil {
		t.Fatalf("empty cutoff should not error: %v", err)
	}
	if _, err := time.Parse("02/01/2006", fallback); err != nil {
		t.Fatalf("expected fallback date in dd/mm/yyyy format, got %q", fallback)
	}
}

func TestExtractNumericField(t *testing.T) {
	if got, ok := extractNumericField("Rp 1,250.50"); !ok || got != 1250.50 {
		t.Fatalf("expected 1250.50 from string parse, got value=%v ok=%v", got, ok)
	}

	if got, ok := extractNumericField(int64(42)); !ok || got != 42 {
		t.Fatalf("expected 42 from int64 parse, got value=%v ok=%v", got, ok)
	}

	if _, ok := extractNumericField(struct{}{}); ok {
		t.Fatal("expected unsupported type to return ok=false")
	}
}

func TestUpdateRecordRejectsVerifiedRecord(t *testing.T) {
	repo := &psoRepoMock{
		getRecord: &domain.OperationalRecord{IsVerified: true},
	}

	svc := NewService(repo, nil, nil, nil)
	err := svc.UpdateRecord(1, map[string]interface{}{"remark": "new"}, 1, "admin")
	if !errors.Is(err, ErrRecordLocked) {
		t.Fatalf("expected ErrRecordLocked, got: %v", err)
	}

	if repo.updatedRecord != nil {
		t.Fatal("did not expect repository UpdateRecord call for locked records")
	}
}

func TestUpdateRecordAllowsSuperadminForVerifiedRecord(t *testing.T) {
	repo := &psoRepoMock{
		getRecord: &domain.OperationalRecord{BaseModel: domain.BaseModel{ID: 11}, IsVerified: true},
	}

	svc := NewService(repo, nil, nil, nil)
	err := svc.UpdateRecord(11, map[string]interface{}{"remark": "corrected"}, 1, "superadmin")
	if err != nil {
		t.Fatalf("expected superadmin to update verified record, got: %v", err)
	}

	if repo.updatedRecord == nil || repo.updatedRecord.Remark != "corrected" {
		t.Fatal("expected repository UpdateRecord call for superadmin override")
	}
}

func TestImportExcelSummaryCountsAndDateParsing(t *testing.T) {
	firstDate := time.Date(2026, time.April, 15, 0, 0, 0, 0, time.UTC)
	existingDate := time.Date(2026, time.May, 1, 0, 0, 0, 0, time.UTC)
	repo := &psoRepoMock{
		getRecordByPsoNo: map[string]*domain.OperationalRecord{
			"SA-002": {
				BaseModel: domain.BaseModel{ID: 22},
				PsoNo:     "SA-002",
				PsoDate:   &existingDate,
			},
		},
	}

	svc := &serviceImpl{repo: repo}
	summary := svc.importExcelRowsWithRepo(repo, [][]string{
		{"NO PSO", "DATE", "NO PO", "CUSTOMER", "TOP", "DISC", "CURRENCY", "AMOUNT", "DAYS", "REMARK"},
		{"SA-001", "15/04/2026", "PO-001", "Customer A", "30", "5.5", "IDR", "1234500", "10", "Baru"},
		{"SA-002", "04/16/2026", "PO-002", "Customer B", "60", "1.0", "USD", "2000", "12", "Update"},
		{"", "04/17/2026", "PO-003", "Customer C", "30", "0", "IDR", "1000", "0", "Skip"},
	}, 7, 0)

	if summary.Processed != 2 || summary.Inserted != 1 || summary.Updated != 1 || summary.Skipped != 1 {
		t.Fatalf("unexpected import summary: %+v", summary)
	}

	if len(repo.upsertedRecords) != 1 {
		t.Fatalf("expected 1 upserted record, got %d", len(repo.upsertedRecords))
	}
	if repo.updatedRecord == nil || repo.updatedRecord.PsoNo != "SA-002" {
		t.Fatal("expected 1 updated record for SA-002")
	}

	if got := repo.upsertedRecords[0]; got.PsoNo != "SA-001" || got.PsoDate == nil || !got.PsoDate.Equal(firstDate) {
		t.Fatalf("unexpected first imported row: %+v", got)
	}

	if got := repo.updatedRecord; got == nil || got.PsoNo != "SA-002" || got.PsoDate == nil || got.PsoDate.Day() != 16 || got.Currency != "USD" {
		t.Fatalf("unexpected second imported row (update): %+v", got)
	}
}

func TestImportPdfSummaryCountsAndDateParsing(t *testing.T) {
	psoDate := time.Date(2026, time.April, 1, 0, 0, 0, 0, time.UTC)
	repo := &psoRepoMock{
		listRecords: []domain.OperationalRecord{{
			BaseModel:    domain.BaseModel{ID: 1},
			PsoNo:        "SA-001",
			PsoDate:      &psoDate,
			PoNo:         "PO-001",
			CustomerName: "ACME",
			TOP:          30,
			Discount:     5.5,
			Amount:       1234500,
			AmountIDR:    1234500,
			Currency:     "IDR",
			Remark:       "ok",
		}},
	}

	svc := NewService(repo, utils.NewPdfGenerator(), nil, nil)
	out, err := svc.ExportPdf("", "2026-04-13", 1)
	if err != nil {
		if strings.Contains(strings.ToLower(err.Error()), "browser executable not found") {
			t.Skipf("skipping PDF generation test: %v", err)
		}
		t.Fatalf("ExportPdf returned error: %v", err)
	}

	pdfFile, err := os.CreateTemp(t.TempDir(), "po-status-*.pdf")
	if err != nil {
		t.Fatalf("CreateTemp failed: %v", err)
	}
	if _, err := pdfFile.Write(out); err != nil {
		_ = pdfFile.Close()
		t.Fatalf("write pdf failed: %v", err)
	}
	if err := pdfFile.Close(); err != nil {
		t.Fatalf("close pdf failed: %v", err)
	}

	importRepo := &psoRepoMock{}
	importSvc := &serviceImpl{repo: importRepo}
	summary := importSvc.importExcelRowsWithRepo(importRepo, nil, 1, 0)
	if summary.Processed != 0 {
		t.Fatalf("expected zero summary for nil input, got %+v", summary)
	}

	loadedSummary, err := importSvc.ImportExcel(pdfFile.Name(), 1, 0)
	if err != nil {
		t.Fatalf("ImportExcel returned error: %v", err)
	}

	if loadedSummary.Processed != 1 || loadedSummary.Inserted != 1 || loadedSummary.Updated != 0 || loadedSummary.Skipped != 0 {
		t.Fatalf("unexpected PDF import summary: %+v", loadedSummary)
	}

	if len(importRepo.upsertedRecords) != 1 {
		t.Fatalf("expected 1 imported record, got %d", len(importRepo.upsertedRecords))
	}

	got := importRepo.upsertedRecords[0]
	if got.PsoNo != "SA-001" || got.PoNo != "PO-001" || got.CustomerName != "ACME" || got.Currency != "IDR" {
		t.Fatalf("unexpected imported PDF row: %+v", got)
	}
	if got.PsoDate == nil || got.PsoDate.Day() != 1 || got.PsoDate.Month() != time.April {
		t.Fatalf("unexpected imported date: %+v", got.PsoDate)
	}
	if got.Amount != 1234500 || got.AmountIDR != 1234500 {
		t.Fatalf("unexpected imported amount: %+v", got)
	}
}

func TestExportExcelReturnsNoDataError(t *testing.T) {
	repo := &psoRepoMock{}
	svc := NewService(repo, nil, nil, nil)

	_, err := svc.ExportExcel("", "2026-04-13", 1)
	if !errors.Is(err, ErrNoExportData) {
		t.Fatalf("expected ErrNoExportData, got: %v", err)
	}
}

func TestExportPdfGeneratesPDF(t *testing.T) {
	psoDate := time.Date(2026, time.April, 1, 0, 0, 0, 0, time.UTC)
	repo := &psoRepoMock{
		listRecords: []domain.OperationalRecord{{
			BaseModel:    domain.BaseModel{ID: 1},
			PsoNo:        "SA-001",
			PsoDate:      &psoDate,
			PoNo:         "PO-001",
			CustomerName: "ACME",
			TOP:          30,
			Discount:     5.5,
			Amount:       1234500,
			AmountIDR:    1234500,
			Currency:     "IDR",
			Remark:       "ok",
		}},
	}

	svc := NewService(repo, utils.NewPdfGenerator(), nil, nil)
	out, err := svc.ExportPdf("", "2026-04-13", 1)
	if err != nil {
		if strings.Contains(strings.ToLower(err.Error()), "browser executable not found") {
			t.Skipf("skipping PDF generation test: %v", err)
		}
		t.Fatalf("ExportPdf returned error: %v", err)
	}

	if !bytes.HasPrefix(out, []byte("%PDF-")) {
		t.Fatalf("expected PDF header, got %q", out[:min(len(out), 8)])
	}
}

func TestExportExcelWritesExpectedRows(t *testing.T) {
	psoDate := time.Date(2026, time.April, 1, 0, 0, 0, 0, time.UTC)
	repo := &psoRepoMock{
		processHandler: func(search string, branchID uint, batchSize int, process func([]domain.OperationalRecord) error) error {
			batch := []domain.OperationalRecord{
				{
					PsoNo:        "SA-001",
					PsoDate:      &psoDate,
					PoNo:         "PO-001",
					CustomerName: "ACME",
					TOP:          30,
					Discount:     5.5,
					Amount:       1234500,
					Remark:       "ok",
				},
			}
			return process(batch)
		},
	}

	svc := NewService(repo, nil, nil, nil)
	out, err := svc.ExportExcel("acme", "2026-04-13", 1)
	if err != nil {
		t.Fatalf("ExportExcel returned error: %v", err)
	}

	book := openWorkbookFromBytes(t, out)

	title, err := book.GetCellValue("Report", "A1")
	if err != nil {
		t.Fatalf("failed to read title cell: %v", err)
	}
	if title != "PO Status Monitoring" {
		t.Fatalf("unexpected title: %q", title)
	}

	subtitle, err := book.GetCellValue("Report", "A2")
	if err != nil {
		t.Fatalf("failed to read subtitle cell: %v", err)
	}
	if !strings.Contains(subtitle, "Print langsung dari data yang sedang tampil di layar.") {
		t.Fatalf("unexpected subtitle: %q", subtitle)
	}

	dataSummary, err := book.GetCellValue("Report", "A3")
	if err != nil {
		t.Fatalf("failed to read data summary cell: %v", err)
	}
	if !strings.Contains(dataSummary, "Data: 1 baris") {
		t.Fatalf("unexpected data summary: %q", dataSummary)
	}

	psoNo, err := book.GetCellValue("Report", "B7")
	if err != nil {
		t.Fatalf("failed to read pso cell: %v", err)
	}
	if psoNo != "SA-001" {
		t.Fatalf("expected SA-001, got %q", psoNo)
	}

	amount, err := book.GetCellValue("Report", "I7")
	if err != nil {
		t.Fatalf("failed to read amount cell: %v", err)
	}
	if amount != "12,345" {
		t.Fatalf("expected amount 12,345, got %q", amount)
	}
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func TestExportPdfReturnsErrorWhenPDFEngineMissing(t *testing.T) {
	repo := &psoRepoMock{}
	svc := NewService(repo, nil, nil, nil)

	_, err := svc.ExportPdf("", "2026-04-13", 1)
	if !errors.Is(err, ErrPDFEngineUnavailable) {
		t.Fatalf("expected ErrPDFEngineUnavailable, got: %v", err)
	}
}

func TestOpenWorkbookHelperRejectsInvalidBytes(t *testing.T) {
	_, err := excelize.OpenReader(bytes.NewReader([]byte("invalid")))
	if err == nil {
		t.Fatal("expected invalid workbook bytes to return error")
	}

	if _, err := io.ReadAll(bytes.NewReader([]byte("ok"))); err != nil {
		t.Fatalf("unexpected io read error: %v", err)
	}
}
