using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ScadaLink.Commons.Messages.Instance;
using ScadaLink.Commons.Types.Flattening;
using ScadaLink.SiteRuntime.Actors;
using ScadaLink.SiteRuntime.Persistence;
using ScadaLink.SiteRuntime.Scripts;
using System.Text.Json;

namespace ScadaLink.SiteRuntime.Tests.Actors;

/// <summary>
/// Tests for InstanceActor: attribute loading, static overrides, and persistence.
/// </summary>
public class InstanceActorTests : TestKit, IDisposable
{
    private readonly SiteStorageService _storage;
    private readonly ScriptCompilationService _compilationService;
    private readonly SharedScriptLibrary _sharedScriptLibrary;
    private readonly SiteRuntimeOptions _options;
    private readonly string _dbFile;

    public InstanceActorTests()
    {
        _dbFile = Path.Combine(Path.GetTempPath(), $"instance-actor-test-{Guid.NewGuid():N}.db");
        _storage = new SiteStorageService(
            $"Data Source={_dbFile}",
            NullLogger<SiteStorageService>.Instance);
        _storage.InitializeAsync().GetAwaiter().GetResult();
        _compilationService = new ScriptCompilationService(
            NullLogger<ScriptCompilationService>.Instance);
        _sharedScriptLibrary = new SharedScriptLibrary(
            _compilationService, NullLogger<SharedScriptLibrary>.Instance);
        _options = new SiteRuntimeOptions();
    }

    private IActorRef CreateInstanceActor(string instanceName, FlattenedConfiguration config)
    {
        return ActorOf(Props.Create(() => new InstanceActor(
            instanceName,
            JsonSerializer.Serialize(config),
            _storage,
            _compilationService,
            _sharedScriptLibrary,
            null, // no stream manager in tests
            _options,
            NullLogger<InstanceActor>.Instance)));
    }

    void IDisposable.Dispose()
    {
        Shutdown();
        try { File.Delete(_dbFile); } catch { /* cleanup */ }
    }

    [Fact]
    public void InstanceActor_LoadsAttributesFromConfig()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Pump1",
            Attributes =
            [
                new ResolvedAttribute { CanonicalName = "Temperature", Value = "98.6", DataType = "Double" },
                new ResolvedAttribute { CanonicalName = "Status", Value = "Running", DataType = "String" }
            ]
        };

        var actor = CreateInstanceActor("Pump1", config);

        // Query for an attribute that exists
        actor.Tell(new GetAttributeRequest(
            "corr-1", "Pump1", "Temperature", DateTimeOffset.UtcNow));

        var response = ExpectMsg<GetAttributeResponse>();
        Assert.True(response.Found);
        Assert.Equal("98.6", response.Value?.ToString());
        Assert.Equal("corr-1", response.CorrelationId);
    }

    [Fact]
    public void InstanceActor_GetAttribute_NotFound_ReturnsFalse()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Pump1",
            Attributes = []
        };

        var actor = CreateInstanceActor("Pump1", config);

        actor.Tell(new GetAttributeRequest(
            "corr-2", "Pump1", "NonExistent", DateTimeOffset.UtcNow));

        var response = ExpectMsg<GetAttributeResponse>();
        Assert.False(response.Found);
        Assert.Null(response.Value);
    }

    [Fact]
    public void InstanceActor_SetStaticAttribute_UpdatesInMemory()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Pump1",
            Attributes =
            [
                new ResolvedAttribute { CanonicalName = "Temperature", Value = "98.6", DataType = "Double" }
            ]
        };

        var actor = CreateInstanceActor("Pump1", config);

        // A static attribute write replies with SetStaticAttributeResponse.
        actor.Tell(new SetStaticAttributeCommand(
            "corr-3", "Pump1", "Temperature", "100.0", DateTimeOffset.UtcNow));
        var setResponse = ExpectMsg<SetStaticAttributeResponse>();
        Assert.True(setResponse.Success);

        // Verify the value changed in memory
        actor.Tell(new GetAttributeRequest(
            "corr-4", "Pump1", "Temperature", DateTimeOffset.UtcNow));

        var getResponse = ExpectMsg<GetAttributeResponse>();
        Assert.True(getResponse.Found);
        Assert.Equal("100.0", getResponse.Value?.ToString());
    }

    [Fact]
    public async Task InstanceActor_SetStaticAttribute_PersistsToSQLite()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "PumpPersist1",
            Attributes =
            [
                new ResolvedAttribute { CanonicalName = "Temperature", Value = "98.6", DataType = "Double" }
            ]
        };

        var actor = CreateInstanceActor("PumpPersist1", config);

        actor.Tell(new SetStaticAttributeCommand(
            "corr-persist", "PumpPersist1", "Temperature", "100.0", DateTimeOffset.UtcNow));

        // A static attribute write replies with SetStaticAttributeResponse once the
        // in-memory state is updated; then wait for the async SQLite persist.
        ExpectMsg<SetStaticAttributeResponse>(TimeSpan.FromSeconds(5));
        await Task.Delay(500);

        // Verify it persisted to SQLite
        var overrides = await _storage.GetStaticOverridesAsync("PumpPersist1");
        Assert.Single(overrides);
        Assert.Equal("100.0", overrides["Temperature"]);
    }

    [Fact]
    public async Task InstanceActor_LoadsStaticOverridesFromSQLite()
    {
        // Pre-populate overrides in SQLite
        await _storage.SetStaticOverrideAsync("PumpOverride1", "Temperature", "200.0");

        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "PumpOverride1",
            Attributes =
            [
                new ResolvedAttribute { CanonicalName = "Temperature", Value = "98.6", DataType = "Double" }
            ]
        };

        var actor = CreateInstanceActor("PumpOverride1", config);

        // Wait for the async override loading to complete (PipeTo)
        await Task.Delay(1000);

        actor.Tell(new GetAttributeRequest(
            "corr-5", "PumpOverride1", "Temperature", DateTimeOffset.UtcNow));

        var response = ExpectMsg<GetAttributeResponse>();
        Assert.True(response.Found);
        // The override value should take precedence over the config default
        Assert.Equal("200.0", response.Value?.ToString());
    }

    [Fact]
    public async Task StaticOverride_ResetOnRedeployment()
    {
        // Set up an override
        await _storage.SetStaticOverrideAsync("PumpRedeploy", "Temperature", "200.0");

        // Verify override exists
        var overrides = await _storage.GetStaticOverridesAsync("PumpRedeploy");
        Assert.Single(overrides);

        // Clear overrides (simulates what DeploymentManager does on redeployment)
        await _storage.ClearStaticOverridesAsync("PumpRedeploy");

        overrides = await _storage.GetStaticOverridesAsync("PumpRedeploy");
        Assert.Empty(overrides);

        // Create actor with fresh config -- should NOT have the override
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "PumpRedeploy",
            Attributes =
            [
                new ResolvedAttribute { CanonicalName = "Temperature", Value = "98.6", DataType = "Double" }
            ]
        };

        var actor = CreateInstanceActor("PumpRedeploy", config);

        await Task.Delay(1000);

        actor.Tell(new GetAttributeRequest(
            "corr-6", "PumpRedeploy", "Temperature", DateTimeOffset.UtcNow));

        var response = ExpectMsg<GetAttributeResponse>();
        Assert.Equal("98.6", response.Value?.ToString());
    }

    [Fact]
    public void InstanceActor_DataSourcedAttribute_StartsWithUncertainQuality()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Pump1",
            Attributes =
            [
                new ResolvedAttribute
                {
                    CanonicalName = "Temperature",
                    Value = "0",
                    DataType = "Double",
                    DataSourceReference = "/Motor/Temperature",
                    BoundDataConnectionName = "OpcServer1"
                }
            ]
        };

        var actor = CreateInstanceActor("Pump1", config);

        actor.Tell(new GetAttributeRequest(
            "corr-quality-1", "Pump1", "Temperature", DateTimeOffset.UtcNow));

        var response = ExpectMsg<GetAttributeResponse>();
        Assert.True(response.Found);
        Assert.Equal("Uncertain", response.Quality);
    }

    [Fact]
    public void InstanceActor_StaticAttribute_StartsWithGoodQuality()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Pump1",
            Attributes =
            [
                new ResolvedAttribute
                {
                    CanonicalName = "Label",
                    Value = "Main Pump",
                    DataType = "String"
                    // No DataSourceReference — static attribute
                }
            ]
        };

        var actor = CreateInstanceActor("Pump1", config);

        actor.Tell(new GetAttributeRequest(
            "corr-quality-2", "Pump1", "Label", DateTimeOffset.UtcNow));

        var response = ExpectMsg<GetAttributeResponse>();
        Assert.True(response.Found);
        Assert.Equal("Good", response.Quality);
    }
}
