using System.Collections.Generic;
using System.Linq;
using System.Text;
using FluentAssertions;
using foodmarket.Pos.Core.Hardware;
using Xunit;
namespace foodmarket.UnitTests;
/// Фейковый COM-порт: эмулирует весы Масса-К по фактам с ноутбука магазина
/// (ENQ→ACK, DC1→кадр). Позволяет проверить разбор кадра и рукопожатие без железа.
public sealed class FakeScaleTransport : ISerialTransport
{
private readonly Queue _rx = new();
public List Written { get; } = new();
public bool Opened { get; private set; }
public bool AnswerAck { get; set; } = true; // отвечает ли ACK на ENQ
public byte[]? Frame { get; set; } // кадр, выдаваемый на DC1
public bool Opening { get; set; } // бросить при Open (порт занят и т.п.)
public int OpenDelayMs { get; set; } // «зависание» на Open (Bluetooth-порт)
public void PreloadRx(params byte[] bytes) { foreach (var b in bytes) _rx.Enqueue(b); }
public void Open()
{
if (OpenDelayMs > 0) System.Threading.Thread.Sleep(OpenDelayMs);
if (Opening) throw new System.UnauthorizedAccessException("busy");
Opened = true;
}
public void DiscardBuffers() { }
public void Write(byte[] data, int offset, int count)
{
for (int i = 0; i < count; i++)
{
var b = data[offset + i];
Written.Add(b);
if (b == MassaK.ENQ && AnswerAck) _rx.Enqueue(MassaK.ACK);
if (b == MassaK.DC1 && Frame is not null) foreach (var fb in Frame) _rx.Enqueue(fb);
}
}
public int ReadByte(int timeoutMs) => _rx.Count > 0 ? _rx.Dequeue() : -1;
public void Dispose() { }
}
public class MassaKScaleTests
{
// Кадр из реального замера: 01 02 'S 0.000kg' 'q' 03 04
private static readonly byte[] Frame0 =
{ 0x01, 0x02, 0x53, 0x20, 0x20, 0x30, 0x2E, 0x30, 0x30, 0x30, 0x6B, 0x67, 0x71, 0x03, 0x04 };
private static byte[] BuildFrame(string payloadText, byte control = 0x71)
{
var body = new List { MassaK.SOH, MassaK.STX };
body.AddRange(Encoding.ASCII.GetBytes(payloadText));
body.Add(control);
body.Add(MassaK.ETX);
body.Add(MassaK.EOT);
return body.ToArray();
}
[Fact]
public void Parses_real_zero_frame_from_shop_laptop()
{
var r = MassaK.ParseFrame(Frame0, out var err);
err.Should().BeNull();
r.Should().NotBeNull();
r!.Value.Kg.Should().Be(0.000m);
r.Value.Flag.Should().Be('S');
r.Value.Unit.Should().Be("kg");
r.Value.Control.Should().Be(0x71);
r.Value.RawHex.Should().StartWith("01 02 53");
}
[Theory]
[InlineData("S 1.235kg", 1.235)]
[InlineData("S 12.500kg", 12.500)]
[InlineData("S 0.000kg", 0.000)]
[InlineData("S -1.235kg", -1.235)]
public void Parses_various_weights(string payload, double expected)
{
var r = MassaK.ParseFrame(BuildFrame(payload), out var err);
err.Should().BeNull();
r!.Value.Kg.Should().Be((decimal)expected);
r.Value.Unit.Should().Be("kg");
}
[Fact]
public void Grams_payload_is_converted_to_kg()
{
var r = MassaK.ParseFrame(BuildFrame("S 1235g"), out _);
r!.Value.Kg.Should().Be(1.235m);
r.Value.Unit.Should().Be("g");
}
[Fact]
public void Skips_garbage_before_soh()
{
var withGarbage = new byte[] { 0xFF, 0x00, 0x7E }.Concat(Frame0).ToArray();
var r = MassaK.ParseFrame(withGarbage, out var err);
err.Should().BeNull();
r!.Value.Kg.Should().Be(0m);
}
[Fact]
public void Truncated_frame_without_etx_is_error()
{
var bad = new byte[] { 0x01, 0x02, 0x53, 0x20, 0x30, 0x2E, 0x30 }; // нет ETX/EOT
var r = MassaK.ParseFrame(bad, out var err);
r.Should().BeNull();
err.Should().Contain("ETX");
}
[Fact]
public void Pure_garbage_without_soh_is_error()
{
var r = MassaK.ParseFrame(new byte[] { 0xAA, 0xBB, 0xCC }, out var err);
r.Should().BeNull();
err.Should().Contain("SOH");
}
[Fact]
public void Handshake_succeeds_when_scale_answers_ack()
{
var t = new FakeScaleTransport { AnswerAck = true };
MassaK.Handshake(t).Should().BeTrue();
t.Written.Should().Contain(MassaK.ENQ);
}
[Fact]
public void Handshake_fails_without_ack()
{
var t = new FakeScaleTransport { AnswerAck = false };
MassaK.Handshake(t, ackTimeoutMs: 50, attempts: 2).Should().BeFalse();
}
[Fact]
public void Full_read_cycle_handshake_then_frame()
{
var t = new FakeScaleTransport { AnswerAck = true, Frame = BuildFrame("S 1.235kg") };
var (reading, hex, err) = MassaK.ReadWeight(t);
err.Should().BeNull();
reading!.Value.Kg.Should().Be(1.235m);
hex.Should().Contain("01 02");
t.Written.Should().Contain(MassaK.DC1);
}
[Fact]
public void Read_cycle_reports_missing_ack()
{
var t = new FakeScaleTransport { AnswerAck = false };
var (reading, _, err) = MassaK.ReadWeight(t, ackTimeoutMs: 50);
reading.Should().BeNull();
err.Should().Contain("ACK");
}
[Fact]
public void ReadStable_detects_stable_and_unstable()
{
// стабильно: один и тот же вес дважды
var stableFrame = BuildFrame("S 2.000kg");
var (kg, stable) = SerialScaleReader.ReadStable("COMX", 57600, ScaleProto.MassaKFrame,
transportFactory: (_, _) => new FakeScaleTransport { AnswerAck = true, Frame = stableFrame }, gapMs: 1);
kg.Should().Be(2.000m);
stable.Should().BeTrue();
}
}