package documents

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

	"github.com/shopspring/decimal"
	"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"
)

type stubRepo struct {
	store storage.DocumentsStore
}

func (s *stubRepo) Ping(ctx 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(ctx context.Context, record *model.DocumentRecord) error {
	s.record = record
	return nil
}

func (s *stubDocumentsStore) Update(ctx context.Context, record *model.DocumentRecord) error {
	s.record = record
	s.updateCalls++
	return nil
}

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

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

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

type memDocStore struct {
	data      map[string][]byte
	saveCount int
	loadCount int
}

func newMemDocStore() *memDocStore {
	return &memDocStore{data: map[string][]byte{}}
}

func (m *memDocStore) Save(ctx context.Context, key string, data []byte) error {
	m.saveCount++
	copyData := make([]byte, len(data))
	copy(copyData, data)
	m.data[key] = copyData
	return nil
}

func (m *memDocStore) Load(ctx context.Context, key string) ([]byte, error) {
	m.loadCount++
	data := m.data[key]
	copyData := make([]byte, len(data))
	copy(copyData, data)
	return copyData, nil
}

func (m *memDocStore) Counts() (int, int) {
	return m.saveCount, m.loadCount
}

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

func (s *stubTemplate) Render(snapshot model.ActSnapshot) ([]renderer.Block, error) {
	s.calls++
	return s.blocks, nil
}

func TestGetDocument_IdempotentAndHashed(t *testing.T) {
	ctx := context.Background()

	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",
	}

	record := &model.DocumentRecord{
		PaymentRef: "PAY-123",
		Snapshot:   snapshot,
	}

	documentsStore := &stubDocumentsStore{record: record}
	repo := &stubRepo{store: documentsStore}
	store := newMemDocStore()
	tmpl := &stubTemplate{
		blocks: []renderer.Block{
			{Tag: renderer.TagTitle, Lines: []string{"ACT"}},
			{Tag: renderer.TagText, Lines: []string{"Executor: Jane Doe", "Amount: 100 USD"}},
		},
	}

	cfg := Config{
		Issuer: renderer.Issuer{
			LegalName:    "Sendico Ltd",
			LegalAddress: "12 Market Street, London, UK",
		},
	}

	svc := NewService(zap.NewNop(), repo, nil,
		WithConfig(cfg),
		WithDocumentStore(store),
		WithTemplateRenderer(tmpl),
	)

	resp1, err := svc.GetDocument(ctx, &documentsv1.GetDocumentRequest{
		PaymentRef: "PAY-123",
		Type:       documentsv1.DocumentType_DOCUMENT_TYPE_ACT,
	})
	if err != nil {
		t.Fatalf("GetDocument first call: %v", err)
	}
	if len(resp1.Content) == 0 {
		t.Fatalf("expected content on first call")
	}

	stored := record.Hashes[model.DocumentTypeAct]
	if stored == "" {
		t.Fatalf("expected stored hash")
	}
	footerHash := extractFooterHash(resp1.Content)
	if footerHash == "" {
		t.Fatalf("expected footer hash in PDF")
	}
	if stored != footerHash {
		t.Fatalf("stored hash mismatch: got %s", stored)
	}

	resp2, err := svc.GetDocument(ctx, &documentsv1.GetDocumentRequest{
		PaymentRef: "PAY-123",
		Type:       documentsv1.DocumentType_DOCUMENT_TYPE_ACT,
	})
	if err != nil {
		t.Fatalf("GetDocument second call: %v", err)
	}
	if !bytes.Equal(resp1.Content, resp2.Content) {
		t.Fatalf("expected identical PDF bytes on second call")
	}

	if tmpl.calls != 1 {
		t.Fatalf("expected template to be rendered once, got %d", tmpl.calls)
	}
	if store.saveCount != 1 {
		t.Fatalf("expected document save once, got %d", store.saveCount)
	}
	if store.loadCount == 0 {
		t.Fatalf("expected document load on second call")
	}
}

func extractFooterHash(pdf []byte) string {
	prefix := []byte("Document integrity hash: ")
	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')
}
