#nullable enable

using System.Collections;
using System.Data;
using System.Data.Common;
using System.Diagnostics.CodeAnalysis;
using MMoneyCoderSharp.Data;
using Oracle.ManagedDataAccess.Client;

namespace MBN_STOCK_WEBVIEW.Infrastructure.Tests;

public sealed class OracleManualFinancialMutationExecutorTests
{
    private const string Version = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";

    [Fact]
    public void ConfigureOracleCommand_sets_bind_by_name_and_rejects_another_provider()
    {
        using var oracleCommand = new OracleCommand();
        OracleManualFinancialMutationExecutor.ConfigureOracleCommand(oracleCommand);
        Assert.True(oracleCommand.BindByName);

        var plan = new Plan();
        using var fake = new FakeCommand(new FakeConnection(plan), plan, 0);
        Assert.Throws<DatabaseConfigurationException>(() =>
            OracleManualFinancialMutationExecutor.ConfigureOracleCommand(fake));
    }

    [Fact]
    public async Task Create_locks_then_executes_bound_command_once_and_commits_one_transaction()
    {
        var plan = new Plan
        {
            ExecuteAsync = (index, _) => Task.FromResult(index == 0 ? -1 : 1)
        };
        var factory = new Factory(plan, 17);
        var service = Service(Executor(factory));

        var receipt = await service.CreateAsync(Sales("Operator ' quoted"));

        Assert.Equal(1, factory.CreateCount);
        Assert.Equal(ManualFinancialMutationKind.Create, receipt.Kind);
        Assert.Equal(ManualFinancialScreenKind.Sales, receipt.Screen);
        Assert.Equal("Operator ' quoted", receipt.StockName);
        Assert.Equal(1, receipt.AffectedRows);
        Assert.NotEqual(Guid.Empty, receipt.OperationId);
        Assert.Equal(1, plan.OpenCount);
        Assert.Equal(1, plan.BeginCount);
        Assert.Equal(IsolationLevel.ReadCommitted, plan.IsolationLevel);
        Assert.Equal(1, plan.CommitCount);
        Assert.Equal(0, plan.RollbackCount);
        Assert.Equal(2, plan.Commands.Count);
        Assert.Equal("LOCK TABLE INPUT_SELL IN EXCLUSIVE MODE", plan.Commands[0].CommandText);
        Assert.Empty(plan.Commands[0].ParametersSnapshot);
        Assert.StartsWith("INSERT INTO INPUT_SELL", plan.Commands[1].CommandText.TrimStart(), StringComparison.Ordinal);
        Assert.DoesNotContain("Operator ' quoted", plan.Commands[1].CommandText, StringComparison.Ordinal);
        Assert.Equal("Operator ' quoted", plan.Commands[1].ParametersSnapshot
            .Single(parameter => parameter.ParameterName == "stock_name").Value);
        Assert.All(plan.Commands, command =>
        {
            Assert.True(command.BindByName);
            Assert.Equal(17, command.CommandTimeout);
            Assert.Equal(1, command.ExecuteCount);
            Assert.Equal(0, command.SynchronousExecuteCount);
            Assert.True(command.WasDisposed);
        });
        Assert.True(plan.ConnectionDisposed);
        Assert.True(plan.TransactionDisposed);
    }

    [Fact]
    public async Task Raw_update_accepts_Core_opaque_ROWID_command_and_binds_legacy_controls()
    {
        var plan = new Plan
        {
            ExecuteAsync = (index, _) => Task.FromResult(index == 0 ? -1 : 1)
        };
        var factory = new Factory(plan, 17);
        var service = new LegacyManualFinancialScreenService(
            new StaticRawQueryExecutor(),
            Executor(factory));
        var listed = Assert.Single((await service.SearchAsync(
            ManualFinancialScreenKind.Sales)).RawItems);
        var values = listed.Record.StorageValues.ToArray();
        values[0] = "  raw_\u0000value ";

        var receipt = await service.UpdateRawAsync(
            listed,
            new ManualFinancialRawRecord(listed.Record.Identity, values));

        Assert.Equal(ManualFinancialMutationKind.Update, receipt.Kind);
        Assert.Equal(2, plan.Commands.Count);
        var command = plan.Commands[1];
        Assert.Contains(
            "ROWID = CHARTOROWID(:row_handle)",
            command.CommandText,
            StringComparison.Ordinal);
        Assert.Equal("  raw_\u0000value ", command.ParametersSnapshot
            .Single(parameter => parameter.ParameterName == "field_1").Value);
        Assert.Equal("AAAAAAAAAAAAAAAAAA", command.ParametersSnapshot
            .Single(parameter => parameter.ParameterName == "row_handle").Value);
    }

    [Fact]
    public async Task Raw_update_row_version_mismatch_rolls_back_once_without_retry()
    {
        var plan = new Plan
        {
            ExecuteAsync = (index, _) => Task.FromResult(index == 0 ? -1 : 0)
        };
        var factory = new Factory(plan, 17);
        var service = new LegacyManualFinancialScreenService(
            new StaticRawQueryExecutor(),
            Executor(factory));
        var listed = Assert.Single((await service.SearchAsync(
            ManualFinancialScreenKind.Sales)).RawItems);
        var values = listed.Record.StorageValues.ToArray();
        values[0] = "A\u001fB";
        values[1] = "C\0";

        var exception = await Assert.ThrowsAsync<ManualFinancialMutationRejectedException>(() =>
            service.UpdateRawAsync(
                listed,
                new ManualFinancialRawRecord(listed.Record.Identity, values)));

        Assert.Equal(
            ManualFinancialMutationRejectionReason.NotFoundOrChanged,
            exception.Reason);
        Assert.False(exception.OutcomeUnknown);
        Assert.Equal(1, factory.CreateCount);
        Assert.Equal(1, plan.RollbackCount);
        Assert.Equal(0, plan.CommitCount);
        Assert.Equal(2, plan.Commands.Count);
        Assert.Equal([1, 1], plan.Commands.Select(command => command.ExecuteCount));
        Assert.Contains("CASE WHEN STOCK_NAME IS NULL THEN 'N'", plan.Commands[1].CommandText,
            StringComparison.Ordinal);
        Assert.DoesNotContain("CHR(31)", plan.Commands[1].CommandText, StringComparison.Ordinal);
    }

    [Fact]
    public async Task DeniedAuthorizationCreatesNoConnectionOrTransaction()
    {
        var plan = new Plan();
        var factory = new Factory(plan);
        var authorization = new DenyAllDatabaseMutationAuthorization();
        var service = Service(Executor(factory, mutationAuthorization: authorization));

        var exception = await Assert.ThrowsAsync<ManualFinancialMutationException>(() =>
            service.CreateAsync(Sales("Alpha")));

        Assert.False(exception.OutcomeUnknown);
        var denied = Assert.IsType<DatabaseMutationAuthorizationException>(
            exception.InnerException);
        Assert.Equal(DatabaseMutationTarget.ManualFinancial, denied.Request.Target);
        Assert.Equal(DataSourceKind.Oracle, denied.Request.Source);
        Assert.Equal(nameof(ManualFinancialMutationKind.Create), denied.Request.MutationKind);
        Assert.NotEqual(Guid.Empty, denied.Request.OperationId);
        Assert.Single(authorization.Requests);
        Assert.Equal(0, factory.CreateCount);
        Assert.Equal(0, plan.OpenCount);
        Assert.Equal(0, plan.BeginCount);
        Assert.Empty(plan.Commands);
    }

    [Fact]
    public async Task Zero_affected_create_rolls_back_as_known_duplicate_before_commit()
    {
        var plan = new Plan
        {
            ExecuteAsync = (index, _) => Task.FromResult(index == 0 ? -1 : 0)
        };
        var factory = new Factory(plan);
        var service = Service(Executor(factory));

        var exception = await Assert.ThrowsAsync<ManualFinancialMutationRejectedException>(() =>
            service.CreateAsync(Sales("Alpha")));

        Assert.False(exception.OutcomeUnknown);
        Assert.Equal(ManualFinancialMutationRejectionReason.DuplicateIdentity, exception.Reason);
        Assert.Equal(1, plan.RollbackCount);
        Assert.Equal(0, plan.CommitCount);
        Assert.Equal(2, plan.Commands.Count);
        Assert.Equal([1, 1], plan.Commands.Select(command => command.ExecuteCount));
        Assert.Equal(1, factory.CreateCount);
    }

    [Fact]
    public async Task Command_timeout_is_OutcomeUnknown_and_is_never_retried()
    {
        var plan = new Plan
        {
            ExecuteAsync = (index, _) => index == 0
                ? Task.FromResult(-1)
                : Task.FromException<int>(new TimeoutException("late"))
        };
        var factory = new Factory(plan);
        var service = Service(Executor(factory));

        var exception = await Assert.ThrowsAsync<ManualFinancialMutationException>(() =>
            service.CreateAsync(Sales("Alpha")));

        Assert.True(exception.OutcomeUnknown);
        Assert.Contains("do not retry", exception.Message, StringComparison.OrdinalIgnoreCase);
        Assert.Equal(1, factory.CreateCount);
        Assert.Equal(1, plan.RollbackCount);
        Assert.Equal(0, plan.CommitCount);
        Assert.Equal(2, plan.Commands.Count);
        Assert.Equal([1, 1], plan.Commands.Select(command => command.ExecuteCount));
    }

    [Fact]
    public async Task Commit_response_loss_is_OutcomeUnknown_without_rollback_or_retry()
    {
        var plan = new Plan
        {
            ExecuteAsync = (index, _) => Task.FromResult(index == 0 ? -1 : 1),
            CommitAsync = _ => Task.FromException(new TestDatabaseException("lost"))
        };
        var factory = new Factory(plan);
        var service = Service(Executor(factory));

        var exception = await Assert.ThrowsAsync<ManualFinancialMutationException>(() =>
            service.DeleteAsync(new ManualFinancialSnapshot(Sales("Alpha"), Version)));

        Assert.True(exception.OutcomeUnknown);
        Assert.Equal(1, plan.CommitCount);
        Assert.Equal(0, plan.RollbackCount);
        Assert.Equal(1, factory.CreateCount);
        Assert.Equal(2, plan.Commands.Count);
    }

    private static LegacyManualFinancialScreenService Service(
        IManualFinancialMutationExecutor executor) =>
        new(new ForbiddenQueryExecutor(), executor);

    private static OracleManualFinancialMutationExecutor Executor(
        Factory factory,
        int operationTimeoutSeconds = 30,
        IDatabaseMutationAuthorization? mutationAuthorization = null) =>
        new(
            factory,
            new DatabaseResilienceOptions
            {
                OperationTimeoutSeconds = operationTimeoutSeconds,
                MaximumRetryCount = 5,
                InitialRetryDelayMilliseconds = 30_000
            },
            new StubErrorDetector(false),
            command => Assert.IsType<FakeCommand>(command).BindByName = true,
            mutationAuthorization ?? DatabaseMutationAuthorization.AllowAll);

    private static ManualSalesRecord Sales(string stockName) =>
        new(
            new ManualFinancialIdentity(ManualFinancialScreenKind.Sales, stockName),
            [
                new("1Q", 10),
                new("2Q", 20),
                new("3Q", 30),
                new("4Q", 40),
                new("5Q", 50),
                new("6Q", 60)
            ]);

    private sealed class ForbiddenQueryExecutor : IDataQueryExecutor
    {
        public DataTable Execute(DataSourceKind source, string tableName, string query) =>
            throw new InvalidOperationException("A mutation test must not read.");

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string tableName,
            string query,
            CancellationToken cancellationToken = default) =>
            throw new InvalidOperationException("A mutation test must not read.");
    }

    private sealed class StaticRawQueryExecutor : IDataQueryExecutor
    {
        public DataTable Execute(DataSourceKind source, string tableName, string query) =>
            throw new NotSupportedException();

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string tableName,
            string query,
            CancellationToken cancellationToken = default) =>
            throw new NotSupportedException();

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string tableName,
            DataQuerySpec query,
            CancellationToken cancellationToken = default)
        {
            var table = new DataTable();
            foreach (var column in ManualFinancialCutContracts.Get(
                         ManualFinancialScreenKind.Sales).StorageColumns)
            {
                table.Columns.Add(column, typeof(string));
            }
            table.Columns.Add("ROW_VERSION", typeof(string));
            table.Columns.Add("ROW_HANDLE", typeof(string));
            table.Rows.Add(
                "Alpha",
                "_10",
                "2Q_20",
                "3Q_30",
                "4Q_40",
                "5Q_50",
                "6Q_60",
                Version,
                "AAAAAAAAAAAAAAAAAA");
            return Task.FromResult(table);
        }
    }

    private sealed class Factory(Plan plan, int commandTimeoutSeconds = 15)
        : IDatabaseConnectionFactory
    {
        public int CreateCount { get; private set; }

        public DbConnection Create(DataSourceKind source)
        {
            Assert.Equal(DataSourceKind.Oracle, source);
            CreateCount++;
            return new FakeConnection(plan);
        }

        public int GetCommandTimeoutSeconds(DataSourceKind source)
        {
            Assert.Equal(DataSourceKind.Oracle, source);
            return commandTimeoutSeconds;
        }
    }

    private sealed class Plan
    {
        public Func<int, CancellationToken, Task<int>> ExecuteAsync { get; init; } =
            (_, _) => Task.FromResult(1);

        public Func<CancellationToken, Task> CommitAsync { get; init; } =
            _ => Task.CompletedTask;

        public Func<CancellationToken, Task> RollbackAsync { get; init; } =
            _ => Task.CompletedTask;

        public int OpenCount { get; set; }

        public int BeginCount { get; set; }

        public int CommitCount { get; set; }

        public int RollbackCount { get; set; }

        public IsolationLevel? IsolationLevel { get; set; }

        public bool ConnectionDisposed { get; set; }

        public bool TransactionDisposed { get; set; }

        public List<FakeCommand> Commands { get; } = [];
    }

    private sealed class FakeConnection(Plan plan) : DbConnection
    {
        private ConnectionState _state = ConnectionState.Closed;

        [AllowNull]
        public override string ConnectionString { get; set; } = string.Empty;

        public override string Database => "Fake";

        public override string DataSource => "Fake";

        public override string ServerVersion => "1";

        public override ConnectionState State => _state;

        public override void ChangeDatabase(string databaseName)
        {
        }

        public override void Close() => _state = ConnectionState.Closed;

        public override void Open() => throw new InvalidOperationException("Synchronous open is forbidden.");

        public override Task OpenAsync(CancellationToken cancellationToken)
        {
            plan.OpenCount++;
            _state = ConnectionState.Open;
            return Task.CompletedTask;
        }

        protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) =>
            throw new InvalidOperationException("Synchronous begin is forbidden.");

        protected override ValueTask<DbTransaction> BeginDbTransactionAsync(
            IsolationLevel isolationLevel,
            CancellationToken cancellationToken)
        {
            plan.BeginCount++;
            plan.IsolationLevel = isolationLevel;
            return ValueTask.FromResult<DbTransaction>(new FakeTransaction(this, plan, isolationLevel));
        }

        protected override DbCommand CreateDbCommand()
        {
            var command = new FakeCommand(this, plan, plan.Commands.Count);
            plan.Commands.Add(command);
            return command;
        }

        public override ValueTask DisposeAsync()
        {
            plan.ConnectionDisposed = true;
            _state = ConnectionState.Closed;
            return ValueTask.CompletedTask;
        }
    }

    private sealed class FakeTransaction(
        DbConnection connection,
        Plan plan,
        IsolationLevel isolationLevel) : DbTransaction
    {
        public override IsolationLevel IsolationLevel => isolationLevel;

        protected override DbConnection DbConnection => connection;

        public override void Commit() => throw new InvalidOperationException("Synchronous commit is forbidden.");

        public override async Task CommitAsync(CancellationToken cancellationToken = default)
        {
            plan.CommitCount++;
            await plan.CommitAsync(cancellationToken);
        }

        public override void Rollback() => throw new InvalidOperationException("Synchronous rollback is forbidden.");

        public override async Task RollbackAsync(CancellationToken cancellationToken = default)
        {
            plan.RollbackCount++;
            await plan.RollbackAsync(cancellationToken);
        }

        public override ValueTask DisposeAsync()
        {
            plan.TransactionDisposed = true;
            return ValueTask.CompletedTask;
        }
    }

    private sealed class FakeCommand(
        DbConnection connection,
        Plan plan,
        int commandIndex) : DbCommand
    {
        private readonly FakeParameterCollection _parameters = new();

        public bool BindByName { get; set; }

        public int ExecuteCount { get; private set; }

        public int SynchronousExecuteCount { get; private set; }

        public bool WasDisposed { get; private set; }

        public IReadOnlyList<DbParameter> ParametersSnapshot =>
            _parameters.Cast<DbParameter>().ToArray();

        [AllowNull]
        public override string CommandText { get; set; } = string.Empty;

        public override int CommandTimeout { get; set; }

        public override CommandType CommandType { get; set; }

        public override bool DesignTimeVisible { get; set; }

        public override UpdateRowSource UpdatedRowSource { get; set; }

        protected override DbConnection? DbConnection { get; set; } = connection;

        protected override DbParameterCollection DbParameterCollection => _parameters;

        protected override DbTransaction? DbTransaction { get; set; }

        public override void Cancel()
        {
        }

        public override int ExecuteNonQuery()
        {
            SynchronousExecuteCount++;
            throw new InvalidOperationException("Synchronous execution is forbidden.");
        }

        public override async Task<int> ExecuteNonQueryAsync(CancellationToken cancellationToken)
        {
            ExecuteCount++;
            return await plan.ExecuteAsync(commandIndex, cancellationToken);
        }

        public override object? ExecuteScalar() => throw new NotSupportedException();

        public override void Prepare()
        {
        }

        protected override DbParameter CreateDbParameter() => new FakeParameter();

        protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) =>
            throw new NotSupportedException();

        public override ValueTask DisposeAsync()
        {
            WasDisposed = true;
            return ValueTask.CompletedTask;
        }
    }

    private sealed class FakeParameter : DbParameter
    {
        public override DbType DbType { get; set; }

        public override ParameterDirection Direction { get; set; }

        public override bool IsNullable { get; set; }

        [AllowNull]
        public override string ParameterName { get; set; } = string.Empty;

        public override int Size { get; set; }

        [AllowNull]
        public override string SourceColumn { get; set; } = string.Empty;

        public override bool SourceColumnNullMapping { get; set; }

        public override object? Value { get; set; }

        public override void ResetDbType() => DbType = default;
    }

    private sealed class FakeParameterCollection : DbParameterCollection
    {
        private readonly List<DbParameter> _parameters = [];

        public override int Count => _parameters.Count;

        public override object SyncRoot => ((ICollection)_parameters).SyncRoot;

        public override int Add(object value)
        {
            _parameters.Add(Require(value));
            return _parameters.Count - 1;
        }

        public override void AddRange(Array values)
        {
            foreach (var value in values)
            {
                Add(value ?? throw new ArgumentNullException(nameof(values)));
            }
        }

        public override void Clear() => _parameters.Clear();

        public override bool Contains(object value) =>
            value is DbParameter parameter && _parameters.Contains(parameter);

        public override bool Contains(string value) => IndexOf(value) >= 0;

        public override void CopyTo(Array array, int index) =>
            ((ICollection)_parameters).CopyTo(array, index);

        public override IEnumerator GetEnumerator() => _parameters.GetEnumerator();

        public override int IndexOf(object value) =>
            value is DbParameter parameter ? _parameters.IndexOf(parameter) : -1;

        public override int IndexOf(string parameterName) =>
            _parameters.FindIndex(parameter => string.Equals(
                parameter.ParameterName,
                parameterName,
                StringComparison.OrdinalIgnoreCase));

        public override void Insert(int index, object value) =>
            _parameters.Insert(index, Require(value));

        public override void Remove(object value)
        {
            if (value is DbParameter parameter)
            {
                _parameters.Remove(parameter);
            }
        }

        public override void RemoveAt(int index) => _parameters.RemoveAt(index);

        public override void RemoveAt(string parameterName)
        {
            var index = IndexOf(parameterName);
            if (index >= 0)
            {
                RemoveAt(index);
            }
        }

        protected override DbParameter GetParameter(int index) => _parameters[index];

        protected override DbParameter GetParameter(string parameterName)
        {
            var index = IndexOf(parameterName);
            return index >= 0 ? _parameters[index] : throw new IndexOutOfRangeException();
        }

        protected override void SetParameter(int index, DbParameter value) =>
            _parameters[index] = value;

        protected override void SetParameter(string parameterName, DbParameter value)
        {
            var index = IndexOf(parameterName);
            if (index >= 0)
            {
                _parameters[index] = value;
            }
            else
            {
                _parameters.Add(value);
            }
        }

        private static DbParameter Require(object value) =>
            value as DbParameter ?? throw new ArgumentException("A parameter is required.");
    }
}
