using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Csv;

namespace ZB.MOM.WW.OtOpcUa.Commons.Tests.Csv;

/// <summary>
///     The required round-trip property: for arbitrary field content, <c>Parse(Write(rows)) == rows</c>.
///     This is what makes the writer a true inverse of the parser and pins the quote-on-demand rules to
///     the parser's grammar. The corpus deliberately includes every tricky character.
/// </summary>
public sealed class CsvRoundTripTests
{
    public static IEnumerable<object[]> Tables()
    {
        // Each case is a jagged table of fields.
        yield return new object[] { new[] { new[] { "a", "b", "c" } } };
        yield return new object[] { new[] { new[] { "a", "b" }, new[] { "c", "d" } } };
        // NOTE: a table whose FINAL row is a single empty field ([[""]], or e.g. [["a"],[""]]) is deliberately
        // excluded — that trailing lone-empty-field row serialises to nothing after the preceding row's
        // terminator, so the parser's no-phantom-trailing-row rule drops it on the way back. This is the only
        // non-round-trippable shape; a single-empty-field row anywhere but last (see the interior [""] case
        // below), ",,", and blank interior lines all round-trip exactly.
        yield return new object[] { new[] { new[] { "", "", "" } } };
        yield return new object[] { new[] { new[] { "a" }, new[] { "" }, new[] { "b" } } };
        yield return new object[] { new[] { new[] { "a,b", "c" } } };                 // embedded delimiter
        yield return new object[] { new[] { new[] { "he said \"hi\"" } } };           // embedded quote
        yield return new object[] { new[] { new[] { "line1\nline2" } } };             // embedded LF
        yield return new object[] { new[] { new[] { "line1\r\nline2" } } };           // embedded CRLF
        yield return new object[] { new[] { new[] { "line1\rline2" } } };             // embedded CR
        yield return new object[] { new[] { new[] { " leading", "trailing " } } };    // spaces
        yield return new object[] { new[] { new[] { "  ", "\t" } } };                 // whitespace-only
        yield return new object[] { new[] { new[] { "\"", "\"\"", ",", "\n" } } };    // every special alone
        yield return new object[] { new[] { new[] { "mix,\"quote\"\nand\r\nnewlines" } } };
        yield return new object[]
        {
            new[]
            {
                new[] { "id", "name", "note" },
                new[] { "1", "alice", "says \"hi, there\"" },
                new[] { "2", "bob", "multi\nline\nnote" },
                new[] { "3", "", "" },
            },
        };
    }

    [Theory]
    [MemberData(nameof(Tables))]
    public void RoundTrips_with_default_crlf(string[][] rows)
    {
        var csv = CsvWriter.WriteToString(rows);
        var parsed = CsvParser.Parse(csv);

        ShouldMatch(parsed, rows);
    }

    [Theory]
    [MemberData(nameof(Tables))]
    public void RoundTrips_with_lf_newline(string[][] rows)
    {
        var csv = CsvWriter.WriteToString(rows, newline: "\n");
        var parsed = CsvParser.Parse(csv);

        ShouldMatch(parsed, rows);
    }

    [Theory]
    [MemberData(nameof(Tables))]
    public void RoundTrips_with_quote_all_fields(string[][] rows)
    {
        var csv = CsvWriter.WriteToString(rows, quoteAllFields: true);
        var parsed = CsvParser.Parse(csv);

        ShouldMatch(parsed, rows);
    }

    [Theory]
    [MemberData(nameof(Tables))]
    public void RoundTrips_with_semicolon_delimiter(string[][] rows)
    {
        var csv = CsvWriter.WriteToString(rows, delimiter: ';');
        var parsed = CsvParser.Parse(csv, ';');

        ShouldMatch(parsed, rows);
    }

    private static void ShouldMatch(IReadOnlyList<string[]> parsed, string[][] expected)
    {
        parsed.Count.ShouldBe(expected.Length);
        for (var i = 0; i < expected.Length; i++)
        {
            parsed[i].ShouldBe(expected[i], $"row {i} mismatch");
        }
    }
}
