package documents

import (
	"bytes"
	"context"
	"strings"
	"testing"
	"time"

	"github.com/shopspring/decimal"
	"github.com/tech/sendico/billing/documents/internal/content"
	"github.com/tech/sendico/billing/documents/renderer"
	"github.com/tech/sendico/billing/documents/storage"
	"github.com/tech/sendico/billing/documents/storage/model"
	documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type stubRepo struct {
	store storage.DocumentsStore
}

func (s *stubRepo) Ping(_ context.Context) error      { return nil }
func (s *stubRepo) Documents() storage.DocumentsStore { return s.store }

var _ storage.Repository = (*stubRepo)(nil)

type stubDocumentsStore struct {
	record      *model.DocumentRecord
	updateCalls int
}

func (s *stubDocumentsStore) Create(_ context.Context, record *model.DocumentRecord) error {
	s.record = record

	return nil
}

func (s *stubDocumentsStore) Update(_ context.Context, record *model.DocumentRecord) error {
	s.record = record
	s.updateCalls++

	return nil
}

func (s *stubDocumentsStore) GetByPaymentRef(_ context.Context, _ string) (*model.DocumentRecord, error) {
	return s.record, nil
}

func (s *stubDocumentsStore) ListByPaymentRefs(_ context.Context, _ []string) ([]*model.DocumentRecord, error) {
	return []*model.DocumentRecord{s.record}, nil
}

var _ storage.DocumentsStore = (*stubDocumentsStore)(nil)

type stubTemplate struct {
	blocks []renderer.Block
	calls  int
}

func (s *stubTemplate) Render(_ model.ActSnapshot) ([]renderer.Block, error) {
	s.calls++

	return s.blocks, nil
}

func TestGenerateActPDF_IdempotentAndHashed(t *testing.T) {
	snapshot := model.ActSnapshot{
		PaymentID:        "PAY-123",
		Date:             time.Date(2026, 1, 30, 0, 0, 0, 0, time.UTC),
		ExecutorFullName: "Jane Doe",
		Amount:           decimal.RequireFromString("100.00"),
		Currency:         "USD",
	}

	tmpl := &stubTemplate{
		blocks: []renderer.Block{
			{Tag: renderer.TagTitle, Lines: []string{"ACT"}},
			{Tag: renderer.TagText, Lines: []string{"Executor: Jane Doe", "Amount: 100 USD"}},
		},
	}

	svc := NewService(zap.NewNop(), nil, nil,
		WithTemplateRenderer(tmpl),
	)

	pdf1, hash1, err := svc.generateActPDF(snapshot)
	if err != nil {
		t.Fatalf("generateActPDF first call: %v", err)
	}

	if len(pdf1) == 0 {
		t.Fatalf("expected content on first call")
	}

	if hash1 == "" {
		t.Fatalf("expected non-empty hash on first call")
	}

	footerHash := extractFooterHash(pdf1)

	if footerHash == "" {
		t.Fatalf("expected footer hash in PDF")
	}

	if hash1 != footerHash {
		t.Fatalf("stored hash mismatch: got %s", hash1)
	}

	pdf2, hash2, err := svc.generateActPDF(snapshot)
	if err != nil {
		t.Fatalf("generateActPDF second call: %v", err)
	}
	if hash2 == "" {
		t.Fatalf("expected non-empty hash on second call")
	}
	footerHash2 := extractFooterHash(pdf2)
	if footerHash2 == "" {
		t.Fatalf("expected footer hash in second PDF")
	}
	if footerHash2 != hash2 {
		t.Fatalf("second hash mismatch: got=%s want=%s", footerHash2, hash2)
	}
}

func extractFooterHash(pdf []byte) string {
	prefix := []byte(content.DocumentIntegrityHashPrefix)
	idx := bytes.Index(pdf, prefix)

	if idx == -1 {
		return ""
	}

	start := idx + len(prefix)

	end := start

	for end < len(pdf) && isHexDigit(pdf[end]) {
		end++
	}

	if end-start != 64 {
		return ""
	}

	return string(pdf[start:end])
}

func isHexDigit(b byte) bool {
	return (b >= '0' && b <= '9') || (b >= 'a' && b <= 'f') || (b >= 'A' && b <= 'F')
}

func TestGetOperationDocument_GeneratesPDF(t *testing.T) {
	svc := NewService(zap.NewNop(), nil, nil)

	resp, err := svc.GetOperationDocument(context.Background(), &documentsv1.GetOperationDocumentRequest{
		OrganizationRef: "org-1",
		GatewayService:  "chain_gateway",
		OperationRef:    "pay-1:step-1",
		PaymentRef:      "pay-1",
		OperationCode:   "crypto.transfer",
		OperationLabel:  "Outbound transfer",
		OperationState:  "completed",
		Amount:          "100.50",
		Currency:        "USDT",
		StartedAtUnixMs: time.Date(2026, 3, 4, 10, 0, 0, 0, time.UTC).UnixMilli(),
	})
	if err != nil {
		t.Fatalf("GetOperationDocument failed: %v", err)
	}
	if len(resp.GetContent()) == 0 {
		t.Fatalf("expected non-empty PDF content")
	}
	if got, want := resp.GetMimeType(), "application/pdf"; got != want {
		t.Fatalf("mime_type mismatch: got=%q want=%q", got, want)
	}
	if got, want := resp.GetFilename(), "operation_pay-1_step-1.pdf"; got != want {
		t.Fatalf("filename mismatch: got=%q want=%q", got, want)
	}
}

func TestGetOperationDocument_RequiresOperationRef(t *testing.T) {
	svc := NewService(zap.NewNop(), nil, nil)

	_, err := svc.GetOperationDocument(context.Background(), &documentsv1.GetOperationDocumentRequest{
		OrganizationRef: "org-1",
		GatewayService:  "chain_gateway",
	})
	if status.Code(err) != codes.InvalidArgument {
		t.Fatalf("expected InvalidArgument, got=%v err=%v", status.Code(err), err)
	}
}

func TestBuildOperationBlocks_CertificateIncludesPaymentData(t *testing.T) {
	snapshot := operationSnapshot{
		OrganizationRef: "org-1",
		GatewayService:  "chain_gateway",
		OperationRef:    "op-123",
		PaymentRef:      "pay-123",
		ClientName:      "Jane Customer",
		ClientAddress:   "Main Street 1, City",
		OperationCode:   "transfer",
		OperationLabel:  "Outbound transfer",
		OperationState:  "completed",
		Amount:          "100.50",
		Currency:        "USDT",
		StartedAt:       time.Date(2026, 3, 1, 10, 0, 0, 0, time.UTC),
		CompletedAt:     time.Date(2026, 3, 2, 12, 0, 0, 0, time.UTC),
	}

	blocks := buildOperationBlocks(snapshot)
	if len(blocks) == 0 {
		t.Fatalf("expected blocks")
	}

	if got := blocks[0].Lines[0]; got != content.OperationDocument.Title {
		t.Fatalf("title mismatch: got=%q want=%q", got, content.OperationDocument.Title)
	}

	meta := findTaggedBlock(blocks, renderer.TagMeta)
	if meta == nil {
		t.Fatalf("expected meta block")
	}

	metaText := strings.Join(meta.Lines, "\n")
	if !strings.Contains(metaText, "Certificate No.: pay-123") {
		t.Fatalf("meta should include certificate number, got=%q", metaText)
	}
	if !strings.Contains(metaText, "Date: March 2, 2026") {
		t.Fatalf("meta should include certificate date, got=%q", metaText)
	}

	amountFound := false
	clientFound := false
	for _, block := range blocks {
		if block.Tag != renderer.TagKV {
			continue
		}

		for _, row := range block.Rows {
			if len(row) >= 2 && row[0] == content.OperationDocument.RowTotalAmount && row[1] == "100.50 USDT" {
				amountFound = true
			}
			if len(row) >= 2 && row[0] == content.OperationDocument.RowClient && row[1] == "Jane Customer" {
				clientFound = true
			}
		}
	}
	if !amountFound {
		t.Fatalf("expected total amount row with payment amount")
	}
	if !clientFound {
		t.Fatalf("expected client row with customer name")
	}
}

func TestCertificateNumber_FallsBackToOperationRef(t *testing.T) {
	got := certificateNumber(operationSnapshot{
		OperationRef: "op-777",
	})

	if got != "op-777" {
		t.Fatalf("certificateNumber fallback mismatch: got=%q want=%q", got, "op-777")
	}
}

func TestCertificateClientName_Fallback(t *testing.T) {
	if got := certificateClientName(operationSnapshot{}); got != "John Doe" {
		t.Fatalf("certificateClientName fallback mismatch: got=%q want=%q", got, "John Doe")
	}
}

func findTaggedBlock(blocks []renderer.Block, tag renderer.Tag) *renderer.Block {
	for i := range blocks {
		if blocks[i].Tag == tag {
			return &blocks[i]
		}
	}

	return nil
}
