using Microsoft.Data.Sqlite;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Tracking;

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Tracking;

/// <summary>
/// LocalDb Phase 1 (Task 2) — the <c>OperationTracking</c> DDL is extracted out of
/// <see cref="OperationTrackingStore"/> so it can also be applied from the Host's
/// <c>AddZbLocalDb</c> onReady callback, which owns table creation for the consolidated
/// site database. These tests pin the extracted schema against the shape the store has
/// always created: the move must be behaviour-preserving.
/// </summary>
public class OperationTrackingSchemaTests
{
    private static SqliteConnection OpenConnection()
    {
        var conn = new SqliteConnection("Data Source=:memory:");
        conn.Open();
        return conn;
    }

    private static List<(string Name, string Type, bool NotNull, bool Pk)> TableInfo(
        SqliteConnection conn, string table)
    {
        using var cmd = conn.CreateCommand();
        cmd.CommandText = $"PRAGMA table_info('{table}')";
        using var reader = cmd.ExecuteReader();
        var cols = new List<(string, string, bool, bool)>();
        while (reader.Read())
        {
            cols.Add((reader.GetString(1), reader.GetString(2), reader.GetInt32(3) == 1,
                reader.GetInt32(5) > 0));
        }

        return cols;
    }

    [Fact]
    public void Apply_CreatesTableWithExpectedColumns()
    {
        using var conn = OpenConnection();

        OperationTrackingSchema.Apply(conn);

        var cols = TableInfo(conn, "OperationTracking");
        Assert.Equal(
            [
                "TrackedOperationId", "Kind", "TargetSummary", "Status", "RetryCount",
                "LastError", "HttpStatus", "CreatedAtUtc", "UpdatedAtUtc", "TerminalAtUtc",
                "SourceInstanceId", "SourceScript", "SourceNode"
            ],
            cols.Select(c => c.Name));
    }

    [Fact]
    public void Apply_UsesTextPrimaryKey()
    {
        using var conn = OpenConnection();

        OperationTrackingSchema.Apply(conn);

        // RegisterReplicated requires an explicit primary key and rejects BLOB
        // columns; a TEXT PK is what makes this table replicate as-is.
        var pk = Assert.Single(TableInfo(conn, "OperationTracking"), c => c.Pk);
        Assert.Equal("TrackedOperationId", pk.Name);
        Assert.Equal("TEXT", pk.Type);
    }

    [Fact]
    public void Apply_HasNoBlobColumns()
    {
        using var conn = OpenConnection();

        OperationTrackingSchema.Apply(conn);

        // ILocalDb.RegisterReplicated throws on any column whose declared type
        // contains BLOB (json_object cannot capture them).
        Assert.DoesNotContain(
            TableInfo(conn, "OperationTracking"),
            c => c.Type.Contains("BLOB", StringComparison.OrdinalIgnoreCase));
    }

    [Fact]
    public void Apply_CreatesExpectedIndexes()
    {
        using var conn = OpenConnection();

        OperationTrackingSchema.Apply(conn);

        using var cmd = conn.CreateCommand();
        cmd.CommandText =
            "SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'OperationTracking' " +
            "AND name NOT LIKE 'sqlite_%' ORDER BY name";
        using var reader = cmd.ExecuteReader();
        var indexes = new List<string>();
        while (reader.Read()) indexes.Add(reader.GetString(0));

        Assert.Equal(
            ["IX_OperationTracking_Status_Updated", "IX_OperationTracking_UpdatedAt"],
            indexes);
    }

    [Fact]
    public void Apply_IsIdempotent()
    {
        using var conn = OpenConnection();

        OperationTrackingSchema.Apply(conn);
        OperationTrackingSchema.Apply(conn);

        Assert.Equal(13, TableInfo(conn, "OperationTracking").Count);
    }

    [Fact]
    public void Apply_AddsSourceNodeToLegacyTableMissingIt()
    {
        using var conn = OpenConnection();

        // A tracking DB created by a pre-SourceNode build. CREATE TABLE IF NOT
        // EXISTS will not add the column, so Apply must ALTER it in — the
        // additive-migration behaviour the store has today must survive extraction.
        using (var legacy = conn.CreateCommand())
        {
            legacy.CommandText = """
                CREATE TABLE OperationTracking (
                    TrackedOperationId TEXT    NOT NULL PRIMARY KEY,
                    Kind               TEXT    NOT NULL,
                    TargetSummary      TEXT    NULL,
                    Status             TEXT    NOT NULL,
                    RetryCount         INTEGER NOT NULL DEFAULT 0,
                    LastError          TEXT    NULL,
                    HttpStatus         INTEGER NULL,
                    CreatedAtUtc       TEXT    NOT NULL,
                    UpdatedAtUtc       TEXT    NOT NULL,
                    TerminalAtUtc      TEXT    NULL,
                    SourceInstanceId   TEXT    NULL,
                    SourceScript       TEXT    NULL
                );
                """;
            legacy.ExecuteNonQuery();
        }

        OperationTrackingSchema.Apply(conn);

        Assert.Contains(TableInfo(conn, "OperationTracking"), c => c.Name == "SourceNode");
    }
}
