package service import ( "reflect" "strings" "testing" ) func TestBuildUSBTicketLayoutPrintsTopToBottom(t *testing.T) { data := usbTicketData{ lotName: "KTI AIRPORT", channelCode: "IN11", plateNo: "2BC1875", entryTime: "30-07-2026 11:14:59", slipNo: "54600", ticketNo: "0123456789abcdef0123456789abcdef", } ops := buildUSBTicketLayout(data) texts := usbTextOperations(ops) want := []string{ "Parking Ticket\n", "KTI AIRPORT\n", strings.Repeat("-", 42) + "\n", "\n", "PARK AT YOUR OWN RISK\n", "KTI AIRPORT A-General Car\n", "In-Time::30-07-2026 11:14:59\n", "InGate::IN11\n", "Slip No::54600\n", "Veh No::2BC1875\n", "SCAN AND PAY WITH NEW\n", "THE CANADIA BANK APP\n", } if !reflect.DeepEqual(texts, want) { t.Fatalf("USB text order = %#v, want %#v", texts, want) } if countText(ops, strings.Repeat("-", 42)+"\n") != 1 { t.Fatalf("expected one 42-character divider, got %d", countText(ops, strings.Repeat("-", 42)+"\n")) } qr, ok := findUSBQRCode(ops) if !ok { t.Fatal("expected a QR-code operation") } if qr.qrData != data.ticketNo || qr.qrWidth != 12 { t.Fatalf("QR operation = (%q, %d), want (%q, 12)", qr.qrData, qr.qrWidth, data.ticketNo) } qrIndex := findUSBQRCodeIndex(ops) paymentIndex := findTextIndex(ops, "SCAN AND PAY WITH NEW\n") plateIndex := findTextIndex(ops, "Veh No::2BC1875\n") cutIndex := findUSBOpKindIndex(ops, usbOpCut) if qrIndex < 0 || paymentIndex < 0 || plateIndex < 0 || cutIndex < 0 || !(plateIndex < qrIndex && qrIndex < paymentIndex && paymentIndex < cutIndex) { t.Fatalf("expected plate before QR before payment before cut, got plate=%d QR=%d payment=%d cut=%d", plateIndex, qrIndex, paymentIndex, cutIndex) } } func TestBuildUSBTicketLayoutOmitsOptionalLines(t *testing.T) { ops := buildUSBTicketLayout(usbTicketData{ lotName: "KTI AIRPORT", entryTime: "30-07-2026 11:14:59", slipNo: "54600", ticketNo: "ticket-no", }) texts := usbTextOperations(ops) for _, text := range texts { if strings.HasPrefix(text, "InGate::") || strings.HasPrefix(text, "Veh No::") { t.Fatalf("optional line should be omitted, got %q", text) } } } func TestBuildUSBTicketLayoutUsesDoubleWidthAroundTitleAndPlate(t *testing.T) { ops := buildUSBTicketLayout(usbTicketData{ lotName: "KTI AIRPORT", plateNo: "2BC1875", entryTime: "30-07-2026 11:14:59", slipNo: "54600", ticketNo: "ticket-no", }) titleIndex := findTextIndex(ops, "Parking Ticket\n") plateIndex := findTextIndex(ops, "Veh No::2BC1875\n") if titleIndex < 2 || plateIndex < 2 { t.Fatalf("title or plate operation is missing") } if !bytesEqual(ops[titleIndex-2].data, []byte{GS, '!', 0x10}) || !bytesEqual(ops[titleIndex+1].data, []byte{GS, '!', 0x00}) { t.Fatalf("title is not bracketed by GS ! double-width/reset operations") } if !bytesEqual(ops[titleIndex-1].data, BoldOnCmd) || !bytesEqual(ops[titleIndex+2].data, BoldOffCmd) { t.Fatalf("title is not bracketed by bold on/off operations") } if !bytesEqual(ops[plateIndex-2].data, []byte{GS, '!', 0x10}) || !bytesEqual(ops[plateIndex+1].data, []byte{GS, '!', 0x00}) { t.Fatalf("plate is not bracketed by GS ! double-width/reset operations") } if !bytesEqual(ops[plateIndex-1].data, BoldOnCmd) || !bytesEqual(ops[plateIndex+2].data, BoldOffCmd) { t.Fatalf("plate is not bracketed by bold on/off operations") } } func TestBuildUSBTicketLayoutUsesNormalTextDirection(t *testing.T) { ops := buildUSBTicketLayout(usbTicketData{ lotName: "Lot#1", plateNo: "2BC1875", entryTime: "03-08-2026 10:00:00", slipNo: "1", ticketNo: "ticket-no", }) directionOff := findUSBWriteDataIndices(ops, []byte{ESC, '{', 0}) directionOn := findUSBWriteDataIndex(ops, []byte{ESC, '{', 1}) qrIndex := findUSBQRCodeIndex(ops) paymentIndex := findTextIndex(ops, "SCAN AND PAY WITH NEW\n") cutIndex := findUSBOpKindIndex(ops, usbOpCut) if len(directionOff) < 2 || directionOn >= 0 || qrIndex < 0 || paymentIndex < 0 || cutIndex < 0 { t.Fatalf("missing normal direction/print operations: off=%d on=%d qr=%d payment=%d cut=%d", len(directionOff), directionOn, qrIndex, paymentIndex, cutIndex) } if !(directionOff[0] < qrIndex && qrIndex < directionOff[1] && directionOff[1] < paymentIndex && paymentIndex < cutIndex) { t.Fatalf("normal direction must precede text and be reapplied after QR: off=%v qr=%d payment=%d cut=%d", directionOff, qrIndex, paymentIndex, cutIndex) } } func TestBuildUSBTicketLayoutFeedsPastCutterBeforeCut(t *testing.T) { ops := buildUSBTicketLayout(usbTicketData{ticketNo: "ticket-no"}) if len(ops) < 2 || ops[len(ops)-2].kind != usbOpFeed || ops[len(ops)-2].count != 8 || ops[len(ops)-1].kind != usbOpCut { t.Fatalf("ticket must end with an 8-line feed and cut, got %#v", ops[len(ops)-2:]) } } func TestExecuteUSBLayoutStopsOnShortWrite(t *testing.T) { device := &recordingUSBDevice{shortWriteAt: 1} ops := []usbPrintOp{ {kind: usbOpWrite, data: []byte("first\n")}, {kind: usbOpWrite, data: []byte("second\n")}, {kind: usbOpQRCode, qrData: "ticket-no", qrWidth: 8}, } if err := executeUSBLayout(device, ops); err == nil { t.Fatal("expected short-write error") } if len(device.writes) != 1 { t.Fatalf("operations after short write executed: %d writes", len(device.writes)) } } func TestExecuteUSBLayoutReturnsQRCodeError(t *testing.T) { device := &recordingUSBDevice{qrOK: false} ops := []usbPrintOp{{kind: usbOpQRCode, qrData: "ticket-no", qrWidth: 8}} if err := executeUSBLayout(device, ops); err == nil { t.Fatal("expected QR-code error") } } type recordingUSBDevice struct { writes [][]byte shortWriteAt int qrOK bool } func (d *recordingUSBDevice) Write(data []byte) int { d.writes = append(d.writes, append([]byte(nil), data...)) if len(d.writes) == d.shortWriteAt { return len(data) - 1 } return len(data) } func (d *recordingUSBDevice) FeedLines(int) {} func (d *recordingUSBDevice) QRCode(string, int) bool { return d.qrOK } func (d *recordingUSBDevice) FullCut() {} func usbTextOperations(ops []usbPrintOp) []string { var texts []string for _, op := range ops { if op.kind == usbOpWrite && len(op.data) > 0 && op.data[len(op.data)-1] == '\n' { texts = append(texts, string(op.data)) } } return texts } func countText(ops []usbPrintOp, want string) int { count := 0 for _, text := range usbTextOperations(ops) { if text == want { count++ } } return count } func findTextIndex(ops []usbPrintOp, want string) int { for i, op := range ops { if op.kind == usbOpWrite && string(op.data) == want { return i } } return -1 } func findUSBWriteDataIndex(ops []usbPrintOp, want []byte) int { for i, op := range ops { if op.kind == usbOpWrite && bytesEqual(op.data, want) { return i } } return -1 } func findUSBWriteDataIndices(ops []usbPrintOp, want []byte) []int { var indices []int for i, op := range ops { if op.kind == usbOpWrite && bytesEqual(op.data, want) { indices = append(indices, i) } } return indices } func findUSBQRCode(ops []usbPrintOp) (usbPrintOp, bool) { for _, op := range ops { if op.kind == usbOpQRCode { return op, true } } return usbPrintOp{}, false } func findUSBQRCodeIndex(ops []usbPrintOp) int { return findUSBOpKindIndex(ops, usbOpQRCode) } func findUSBOpKindIndex(ops []usbPrintOp, kind usbPrintOpKind) int { for i, op := range ops { if op.kind == kind { return i } } return -1 } func bytesEqual(a, b []byte) bool { return reflect.DeepEqual(a, b) }