using Shouldly;
using Xunit;

namespace ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser.Tests;

/// <summary>
///     Pins the browse NodeId encoding. This codec is the load-bearing detail of the whole picker: a NodeId
///     that mis-parses sends the operator to a different column than the one they clicked, and the resulting
///     tag polls the wrong data forever with no error anywhere. SQL Server identifiers may legally contain
///     <c>.</c> and <c>|</c> (inside a quoted identifier), so the round-trip has to hold for those too.
/// </summary>
public sealed class SqlBrowseNodeIdTests
{
    [Fact]
    public void ForSchema_roundTrips()
    {
        var reference = SqlBrowseNodeId.Parse(SqlBrowseNodeId.ForSchema("dbo"));

        reference.Kind.ShouldBe(SqlBrowseNodeKind.Schema);
        reference.Schema.ShouldBe("dbo");
        reference.Table.ShouldBeNull();
        reference.Column.ShouldBeNull();
    }

    [Fact]
    public void ForTable_roundTrips()
    {
        var reference = SqlBrowseNodeId.Parse(SqlBrowseNodeId.ForTable("dbo", "TagValues"));

        reference.Kind.ShouldBe(SqlBrowseNodeKind.Table);
        reference.Schema.ShouldBe("dbo");
        reference.Table.ShouldBe("TagValues");
        reference.Column.ShouldBeNull();
    }

    [Fact]
    public void ForColumn_roundTrips()
    {
        var reference = SqlBrowseNodeId.Parse(SqlBrowseNodeId.ForColumn("dbo", "TagValues", "num_value"));

        reference.Kind.ShouldBe(SqlBrowseNodeKind.Column);
        reference.Schema.ShouldBe("dbo");
        reference.Table.ShouldBe("TagValues");
        reference.Column.ShouldBe("num_value");
    }

    /// <summary>
    ///     The exact case the plan's <c>schema.table|column</c> sketch cannot express: <c>main.a.b|c</c> reads
    ///     equally as schema <c>main</c> + table <c>a.b</c> and as schema <c>main.a</c> + table <c>b</c>.
    /// </summary>
    [Theory]
    [InlineData("a.b", "c", "d")]
    [InlineData("a", "b.c", "d")]
    [InlineData("a", "b", "c.d")]
    [InlineData("a|b", "c", "d")]
    [InlineData("a", "b|c", "d")]
    [InlineData("a", "b", "c|d")]
    [InlineData(@"a\b", "c", "d")]
    [InlineData("a", @"b\|c", "d")]
    [InlineData("a", "b", @"c\\d")]
    [InlineData("a.b|c", "d.e|f", "g.h|i")]
    [InlineData("schema", "table", "column")]
    [InlineData("x:y", "z:w", "q:r")]
    public void AwkwardIdentifiers_roundTripExactly(string schema, string table, string column)
    {
        var reference = SqlBrowseNodeId.Parse(SqlBrowseNodeId.ForColumn(schema, table, column));

        reference.Kind.ShouldBe(SqlBrowseNodeKind.Column);
        reference.Schema.ShouldBe(schema);
        reference.Table.ShouldBe(table);
        reference.Column.ShouldBe(column);
    }

    /// <summary>
    ///     Two genuinely different columns must never collide on one NodeId. Under the plan's literal
    ///     <c>schema.table|column</c> sketch both of these render as <c>a.b|c</c>.
    /// </summary>
    [Fact]
    public void DistinctReferences_neverCollide()
    {
        var nested = SqlBrowseNodeId.ForColumn("a.b", "t", "c");
        var flat = SqlBrowseNodeId.ForColumn("a", "b.t", "c");

        nested.ShouldNotBe(flat);
        SqlBrowseNodeId.Parse(nested).Schema.ShouldBe("a.b");
        SqlBrowseNodeId.Parse(flat).Schema.ShouldBe("a");
    }

    /// <summary>A table NodeId and a schema NodeId are never confusable, whatever the names contain.</summary>
    [Fact]
    public void KindsAreDistinguishable()
    {
        SqlBrowseNodeId.Parse(SqlBrowseNodeId.ForSchema("a|b")).Kind.ShouldBe(SqlBrowseNodeKind.Schema);
        SqlBrowseNodeId.Parse(SqlBrowseNodeId.ForTable("a", "b")).Kind.ShouldBe(SqlBrowseNodeKind.Table);
        SqlBrowseNodeId.ForSchema("a|b").ShouldNotBe(SqlBrowseNodeId.ForTable("a", "b"));
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData("main")]                 // no kind prefix
    [InlineData("bogus:main")]           // unknown kind
    [InlineData("table:main")]           // table kind with only one part
    [InlineData("schema:main|extra")]    // schema kind with two parts
    [InlineData("column:a|b")]           // column kind with two parts
    [InlineData("column:a|b|c|d")]       // column kind with four parts
    [InlineData("schema:")]              // empty part
    [InlineData("table:main|")]          // empty trailing part
    [InlineData(@"schema:main\")]        // dangling escape
    [InlineData(":main")]                // empty kind
    public void MalformedNodeIds_areRejected(string? nodeId)
    {
        SqlBrowseNodeId.TryParse(nodeId, out _).ShouldBeFalse();
        Should.Throw<ArgumentException>(() => SqlBrowseNodeId.Parse(nodeId));
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("  ")]
    public void EmptyIdentifiers_areRejectedAtEncode(string? identifier)
    {
        Should.Throw<ArgumentException>(() => SqlBrowseNodeId.ForSchema(identifier!));
        Should.Throw<ArgumentException>(() => SqlBrowseNodeId.ForTable("main", identifier!));
        Should.Throw<ArgumentException>(() => SqlBrowseNodeId.ForColumn("main", "t", identifier!));
    }
}
