using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Commons.Types;

namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests;

/// <summary>
/// Secret-elision projection tests (arch-review C3): connection-config secrets
/// (OPC UA passwords, external-system auth config, DB connection strings) must
/// never leave the management boundary via List/Get responses or audit afterState,
/// and sentinel/omitted round-trips through Update must preserve the stored secret.
/// </summary>
public class SecretProjectionTests : TestKit, IDisposable
{
    private readonly IAuditService _auditService;
    private readonly ServiceCollection _services;

    public SecretProjectionTests()
    {
        _auditService = Substitute.For<IAuditService>();
        _services = new ServiceCollection();
        _services.AddScoped(_ => _auditService);
    }

    private IActorRef CreateActor()
    {
        var sp = _services.BuildServiceProvider();
        return Sys.ActorOf(Props.Create(() => new ManagementActor(
            sp, NullLogger<ManagementActor>.Instance)));
    }

    private static ManagementEnvelope Envelope(object command, params string[] roles) =>
        new(new AuthenticatedUser("testuser", "Test User", roles, Array.Empty<string>()),
            command, Guid.NewGuid().ToString("N"));

    void IDisposable.Dispose() => Shutdown();

    // ========================================================================
    // Task 7 — DataConnection config secret elision
    // ========================================================================

    private ISiteRepository AddSiteRepoWithConnection(DataConnection conn)
    {
        var siteRepo = Substitute.For<ISiteRepository>();
        siteRepo.GetDataConnectionByIdAsync(conn.Id, Arg.Any<CancellationToken>()).Returns(conn);
        siteRepo.GetAllDataConnectionsAsync(Arg.Any<CancellationToken>())
            .Returns(new List<DataConnection> { conn });
        _services.AddScoped(_ => siteRepo);
        return siteRepo;
    }

    [Fact]
    public void GetDataConnection_ResponseOmitsPasswords()
    {
        AddSiteRepoWithConnection(new DataConnection("c1", "OpcUa", 1)
        {
            Id = 1,
            PrimaryConfiguration = """{"Endpoint":"opc.tcp://x","UserIdentity":{"Password":"opc-secret"}}""",
        });

        var actor = CreateActor();
        actor.Tell(Envelope(new GetDataConnectionCommand(1), "Viewer"));

        var resp = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
        Assert.DoesNotContain("opc-secret", resp.JsonData);
        Assert.Contains("hasSecrets", resp.JsonData); // camelCase serializer
    }

    [Fact]
    public void ListDataConnections_ResponseOmitsPasswords()
    {
        AddSiteRepoWithConnection(new DataConnection("c1", "OpcUa", 1)
        {
            Id = 1,
            PrimaryConfiguration = """{"UserIdentity":{"Password":"opc-secret"}}""",
        });

        var actor = CreateActor();
        actor.Tell(Envelope(new ListDataConnectionsCommand(null), "Viewer"));

        var resp = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
        Assert.DoesNotContain("opc-secret", resp.JsonData);
        Assert.Contains("hasSecrets", resp.JsonData);
    }

    [Fact]
    public async Task UpdateDataConnection_SentinelConfig_PreservesStoredSecret()
    {
        var stored = new DataConnection("c1", "OpcUa", 1)
        {
            Id = 1,
            PrimaryConfiguration = """{"Endpoint":"opc.tcp://old","UserIdentity":{"Password":"opc-secret"}}""",
        };
        var siteRepo = AddSiteRepoWithConnection(stored);

        var actor = CreateActor();
        var incoming = """{"Endpoint":"opc.tcp://new","UserIdentity":{"Password":"***REDACTED***"}}""";
        actor.Tell(Envelope(new UpdateDataConnectionCommand(1, "c1", "OpcUa", incoming, null, 3), "Designer"));
        ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));

        await siteRepo.Received(1).UpdateDataConnectionAsync(
            Arg.Is<DataConnection>(c => c.PrimaryConfiguration!.Contains("opc-secret")
                                     && !c.PrimaryConfiguration.Contains("***REDACTED***")),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public void UpdateDataConnection_AuditAfterState_OmitsPasswords()
    {
        var stored = new DataConnection("c1", "OpcUa", 1)
        {
            Id = 1,
            PrimaryConfiguration = """{"Password":"opc-secret"}""",
        };
        AddSiteRepoWithConnection(stored);

        var actor = CreateActor();
        actor.Tell(Envelope(new UpdateDataConnectionCommand(1, "c1", "OpcUa", """{"Password":"newpw"}""", null, 3), "Designer"));
        ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));

        _auditService.Received().LogAsync(Arg.Any<string>(), "Update", "DataConnection",
            Arg.Any<string>(), Arg.Any<string>(),
            Arg.Is<object>(o => !ManagementActor.SerializeResult(o).Contains("newpw")),
            Arg.Any<CancellationToken>());
    }

    // ========================================================================
    // Task 8 — ExternalSystem AuthConfiguration elision
    // ========================================================================

    private IExternalSystemRepository AddExternalSystemRepo(ExternalSystemDefinition def)
    {
        var repo = Substitute.For<IExternalSystemRepository>();
        repo.GetExternalSystemByIdAsync(def.Id, Arg.Any<CancellationToken>()).Returns(def);
        repo.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
            .Returns(new List<ExternalSystemDefinition> { def });
        _services.AddScoped(_ => repo);
        return repo;
    }

    [Fact]
    public void GetExternalSystem_ResponseOmitsAuthConfiguration()
    {
        AddExternalSystemRepo(new ExternalSystemDefinition("es", "https://x", "ApiKey")
        {
            Id = 1,
            AuthConfiguration = """{"apiKey":"es-secret"}""",
        });

        var actor = CreateActor();
        actor.Tell(Envelope(new GetExternalSystemCommand(1), "Viewer"));

        var resp = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
        Assert.DoesNotContain("es-secret", resp.JsonData);
        Assert.Contains("hasAuthConfiguration", resp.JsonData);
    }

    [Fact]
    public void ListExternalSystems_ResponseOmitsAuthConfiguration()
    {
        AddExternalSystemRepo(new ExternalSystemDefinition("es", "https://x", "ApiKey")
        {
            Id = 1,
            AuthConfiguration = """{"apiKey":"es-secret"}""",
        });

        var actor = CreateActor();
        actor.Tell(Envelope(new ListExternalSystemsCommand(), "Viewer"));

        var resp = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
        Assert.DoesNotContain("es-secret", resp.JsonData);
        Assert.Contains("hasAuthConfiguration", resp.JsonData);
    }

    [Fact]
    public async Task UpdateExternalSystem_NullAuthConfiguration_PreservesStoredSecret()
    {
        var stored = new ExternalSystemDefinition("es", "https://x", "ApiKey")
        {
            Id = 1,
            AuthConfiguration = """{"apiKey":"es-secret"}""",
        };
        var repo = AddExternalSystemRepo(stored);

        var actor = CreateActor();
        actor.Tell(Envelope(new UpdateExternalSystemCommand(1, "es", "https://y", "ApiKey", null), "Designer"));
        ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));

        await repo.Received(1).UpdateExternalSystemAsync(
            Arg.Is<ExternalSystemDefinition>(d => d.AuthConfiguration == """{"apiKey":"es-secret"}"""),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public void UpdateExternalSystem_AuditAfterState_OmitsAuthConfiguration()
    {
        var stored = new ExternalSystemDefinition("es", "https://x", "ApiKey")
        {
            Id = 1,
            AuthConfiguration = """{"apiKey":"es-secret"}""",
        };
        AddExternalSystemRepo(stored);

        var actor = CreateActor();
        actor.Tell(Envelope(new UpdateExternalSystemCommand(1, "es", "https://y", "ApiKey", """{"apiKey":"newsecret"}"""), "Designer"));
        ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));

        _auditService.Received().LogAsync(Arg.Any<string>(), "Update", "ExternalSystem",
            Arg.Any<string>(), Arg.Any<string>(),
            Arg.Is<object>(o => !ManagementActor.SerializeResult(o).Contains("newsecret")),
            Arg.Any<CancellationToken>());
    }

    // ========================================================================
    // Task 9 — DatabaseConnection ConnectionString elision
    // ========================================================================

    private IExternalSystemRepository AddDatabaseConnectionRepo(DatabaseConnectionDefinition def)
    {
        var repo = Substitute.For<IExternalSystemRepository>();
        repo.GetDatabaseConnectionByIdAsync(def.Id, Arg.Any<CancellationToken>()).Returns(def);
        repo.GetAllDatabaseConnectionsAsync(Arg.Any<CancellationToken>())
            .Returns(new List<DatabaseConnectionDefinition> { def });
        _services.AddScoped(_ => repo);
        return repo;
    }

    [Fact]
    public void GetDatabaseConnection_ResponseOmitsConnectionString()
    {
        AddDatabaseConnectionRepo(new DatabaseConnectionDefinition("db", "Server=db;Password=sql-secret") { Id = 1 });

        var actor = CreateActor();
        actor.Tell(Envelope(new GetDatabaseConnectionCommand(1), "Viewer"));

        var resp = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
        Assert.DoesNotContain("sql-secret", resp.JsonData);
        Assert.Contains("hasConnectionString", resp.JsonData);
    }

    [Fact]
    public void ListDatabaseConnections_ResponseOmitsConnectionString()
    {
        AddDatabaseConnectionRepo(new DatabaseConnectionDefinition("db", "Server=db;Password=sql-secret") { Id = 1 });

        var actor = CreateActor();
        actor.Tell(Envelope(new ListDatabaseConnectionsCommand(), "Viewer"));

        var resp = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
        Assert.DoesNotContain("sql-secret", resp.JsonData);
        Assert.Contains("hasConnectionString", resp.JsonData);
    }

    [Fact]
    public async Task UpdateDatabaseConnection_NullConnectionString_PreservesStoredSecret()
    {
        var stored = new DatabaseConnectionDefinition("db", "Server=db;Password=sql-secret") { Id = 1 };
        var repo = AddDatabaseConnectionRepo(stored);

        var actor = CreateActor();
        actor.Tell(Envelope(new UpdateDatabaseConnectionDefCommand(1, "db-renamed", null), "Designer"));
        ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));

        await repo.Received(1).UpdateDatabaseConnectionAsync(
            Arg.Is<DatabaseConnectionDefinition>(d => d.ConnectionString == "Server=db;Password=sql-secret"),
            Arg.Any<CancellationToken>());
    }
}
