using ZB.MOM.WW.ScadaBridge.Commons.Types; namespace ZB.MOM.WW.ScadaBridge.Commons.Tests; public class OverrideCsvParserTests { [Fact] public void Parse_SimpleThreeColumnFile_ReturnsTwoRowsNoErrors() { const string csv = "AttributeName,Value,ElementType\nSetpoint,42,Int32\nName,Pump A,\n"; var result = OverrideCsvParser.Parse(csv); Assert.Empty(result.Errors); Assert.Equal(2, result.Rows.Count); Assert.Equal("Setpoint", result.Rows[0].AttributeName); Assert.Equal("42", result.Rows[0].Value); Assert.Equal("Int32", result.Rows[0].ElementType); Assert.Equal(2, result.Rows[0].LineNumber); Assert.Equal("Name", result.Rows[1].AttributeName); Assert.Equal("Pump A", result.Rows[1].Value); Assert.Null(result.Rows[1].ElementType); Assert.Equal(3, result.Rows[1].LineNumber); } [Fact] public void Parse_TwoColumnFileWithoutElementType_RowsHaveNullElementType() { const string csv = "AttributeName,Value\nSetpoint,42\nName,Pump A\n"; var result = OverrideCsvParser.Parse(csv); Assert.Empty(result.Errors); Assert.Equal(2, result.Rows.Count); Assert.All(result.Rows, r => Assert.Null(r.ElementType)); Assert.Equal("42", result.Rows[0].Value); Assert.Equal("Pump A", result.Rows[1].Value); } [Fact] public void Parse_QuotedValueWithComma_PreservesEmbeddedComma() { const string csv = "AttributeName,Value,ElementType\nName,\"a,b,c\",\n"; var result = OverrideCsvParser.Parse(csv); Assert.Empty(result.Errors); var row = Assert.Single(result.Rows); Assert.Equal("Name", row.AttributeName); Assert.Equal("a,b,c", row.Value); Assert.Null(row.ElementType); } [Fact] public void Parse_DoubledQuoteEscape_UnescapesToSingleQuote() { const string csv = "AttributeName,Value,ElementType\nName,\"he said \"\"hi\"\"\",\n"; var result = OverrideCsvParser.Parse(csv); Assert.Empty(result.Errors); var row = Assert.Single(result.Rows); Assert.Equal("he said \"hi\"", row.Value); } [Fact] public void Parse_EmptyValueField_YieldsNullValue() { const string csv = "AttributeName,Value,ElementType\nSetpoint,,\n"; var result = OverrideCsvParser.Parse(csv); Assert.Empty(result.Errors); var row = Assert.Single(result.Rows); Assert.Equal("Setpoint", row.AttributeName); Assert.Null(row.Value); Assert.Null(row.ElementType); } [Fact] public void Parse_RowWithTooFewColumns_ProducesLineNumberedErrorAndExcludesRow() { const string csv = "AttributeName,Value,ElementType\nSetpoint\nName,Pump A,\n"; var result = OverrideCsvParser.Parse(csv); // Bad row on line 2 excluded; good row on line 3 retained. var row = Assert.Single(result.Rows); Assert.Equal("Name", row.AttributeName); Assert.Equal(3, row.LineNumber); var error = Assert.Single(result.Errors); Assert.Contains("2", error); } [Fact] public void Parse_BlankAttributeName_ProducesLineNumberedError() { const string csv = "AttributeName,Value,ElementType\n,42,Int32\n"; var result = OverrideCsvParser.Parse(csv); Assert.Empty(result.Rows); var error = Assert.Single(result.Errors); Assert.Contains("2", error); } [Fact] public void Parse_BlankLines_AreSkippedWithoutError() { const string csv = "AttributeName,Value,ElementType\n\nSetpoint,42,Int32\n \nName,Pump A,\n"; var result = OverrideCsvParser.Parse(csv); Assert.Empty(result.Errors); Assert.Equal(2, result.Rows.Count); // LineNumber reflects the true source line (blank line 2 skipped). Assert.Equal(3, result.Rows[0].LineNumber); Assert.Equal(5, result.Rows[1].LineNumber); } [Fact] public void Parse_MissingHeader_ReturnsZeroRowsAndHeaderError() { const string csv = "Setpoint,42,Int32\nName,Pump A,\n"; var result = OverrideCsvParser.Parse(csv); Assert.Empty(result.Rows); var error = Assert.Single(result.Errors); Assert.Contains("header", error, StringComparison.OrdinalIgnoreCase); } [Fact] public void Parse_HeaderIsCaseInsensitiveAndTrimsWhitespace() { const string csv = "attributename, value , elementtype\nSetpoint,42,Int32\n"; var result = OverrideCsvParser.Parse(csv); Assert.Empty(result.Errors); var row = Assert.Single(result.Rows); Assert.Equal("Setpoint", row.AttributeName); Assert.Equal("42", row.Value); Assert.Equal("Int32", row.ElementType); } [Fact] public void Parse_EmptyInput_ReturnsHeaderError() { var result = OverrideCsvParser.Parse(string.Empty); Assert.Empty(result.Rows); Assert.Single(result.Errors); } [Fact] public void Parse_UnquotedWhitespace_IsTrimmedButQuotedWhitespacePreserved() { const string csv = "AttributeName,Value,ElementType\n Setpoint , 42 ,Int32\nName,\" spaced \",\n"; var result = OverrideCsvParser.Parse(csv); Assert.Empty(result.Errors); Assert.Equal(2, result.Rows.Count); Assert.Equal("Setpoint", result.Rows[0].AttributeName); Assert.Equal("42", result.Rows[0].Value); Assert.Equal(" spaced ", result.Rows[1].Value); } [Fact] public void Parse_MidFieldUnquotedQuote_IsPreservedAsLiteral() { // A '"' that does NOT open a field (it follows non-whitespace content in an // unquoted field) is a literal character — 'va"lue' must survive intact. const string csv = "AttributeName,Value,ElementType\nName,va\"lue,Type\n"; var result = OverrideCsvParser.Parse(csv); Assert.Empty(result.Errors); var row = Assert.Single(result.Rows); Assert.Equal("Name", row.AttributeName); Assert.Equal("va\"lue", row.Value); Assert.Equal("Type", row.ElementType); } [Fact] public void Parse_UnterminatedQuotedField_ProducesLineNumberedErrorAndExcludesRow() { // A quote opens the field but is never closed before end-of-line → malformed. const string csv = "AttributeName,Value\nName,\"unclosed\n"; var result = OverrideCsvParser.Parse(csv); Assert.Empty(result.Rows); var error = Assert.Single(result.Errors); Assert.Contains("2", error); Assert.Contains("Unterminated", error, StringComparison.OrdinalIgnoreCase); } [Fact] public void Parse_WellFormedQuotedFieldWithComma_StillParses() { // Regression guard alongside the unterminated-quote fix: a properly closed // quoted field embedding a comma must still round-trip. const string csv = "AttributeName,Value,ElementType\nName,\"a,b\",Type\n"; var result = OverrideCsvParser.Parse(csv); Assert.Empty(result.Errors); var row = Assert.Single(result.Rows); Assert.Equal("Name", row.AttributeName); Assert.Equal("a,b", row.Value); Assert.Equal("Type", row.ElementType); } [Fact] public void Parse_EmptyQuotedField_YieldsNullValue() { // A bare "" is an empty quoted field; empty → null per the empty-field rule. const string csv = "AttributeName,Value,ElementType\nName,\"\",Type\n"; var result = OverrideCsvParser.Parse(csv); Assert.Empty(result.Errors); var row = Assert.Single(result.Rows); Assert.Equal("Name", row.AttributeName); Assert.Null(row.Value); Assert.Equal("Type", row.ElementType); } [Fact] public void Parse_QuotedFieldWithTrailingWhitespaceAfterClose_IsAccepted() { // The closing quote may be followed by ignorable trailing whitespace before // the delimiter; the field value itself is preserved verbatim. const string csv = "AttributeName,Value,ElementType\nName,\"a,b\" ,Type\n"; var result = OverrideCsvParser.Parse(csv); Assert.Empty(result.Errors); var row = Assert.Single(result.Rows); Assert.Equal("a,b", row.Value); Assert.Equal("Type", row.ElementType); } }