using System;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using PolyTrader.Core.Configuration;
using PolyTraderSharp.Services;
using Xunit;

namespace PolyTrader.Tests
{
    /// <summary>
    /// Sicherheitsnetz für D-3 (Fehler-Reporting an das Deploymentcenter). Kernpunkte, die hier
    /// fixiert werden: TLS-Zwang wie beim Watchdog, korrekter Endpunkt/Header, Secrets werden aus
    /// Nachricht UND Stacktrace entfernt, lokale Ratenbegrenzung, und die drei Fehlerquellen
    /// (App-Log, AppDomain.UnhandledException, TaskScheduler.UnobservedTaskException) melden mit
    /// dem jeweils richtigen <c>level</c>.
    /// </summary>
    public class DeploymentcenterErrorReporterTests
    {
        // ----- Grundfunktion -----

        [Fact]
        public async Task ReportAsync_posts_expected_payload_and_auth_header()
        {
            var reporter = Build(out FakeHandler handler, Enabled());

            bool ok = await reporter.ReportAsync("System.InvalidOperationException", "Etwas kaputt", "at Foo.Bar()", "error");

            Assert.True(ok);
            Assert.Equal("https://dc.example.de/api/errors/v1/report", handler.LastRequest!.RequestUri!.AbsoluteUri);
            Assert.Equal("Bearer", handler.LastRequest.Headers.Authorization!.Scheme);
            Assert.Equal("secret-token", handler.LastRequest.Headers.Authorization!.Parameter);

            using var doc = JsonDocument.Parse(handler.LastBody!);
            var root = doc.RootElement;
            Assert.Equal("polytrader", root.GetProperty("project_slug").GetString());
            Assert.Equal("System.InvalidOperationException", root.GetProperty("exception").GetString());
            Assert.Equal("Etwas kaputt", root.GetProperty("message").GetString());
            Assert.Equal("at Foo.Bar()", root.GetProperty("stack_trace").GetString());
            Assert.Equal("error", root.GetProperty("level").GetString());
            Assert.False(string.IsNullOrWhiteSpace(root.GetProperty("build").GetString()));
            Assert.False(string.IsNullOrWhiteSpace(root.GetProperty("environment").GetString()));
        }

        [Fact]
        public async Task ReportAsync_does_nothing_when_disabled()
        {
            var options = Enabled();
            options.ErrorReportingEnabled = false;
            var reporter = Build(out FakeHandler handler, options);

            bool ok = await reporter.ReportAsync("X", "msg", null, "error");

            Assert.False(ok);
            Assert.Null(handler.LastRequest);
        }

        [Theory]
        [InlineData("http://dc.example.de")]
        [InlineData("")]
        public async Task ReportAsync_rejects_insecure_or_missing_url(string baseUrl)
        {
            var options = Enabled();
            options.BaseUrl = baseUrl;
            var reporter = Build(out FakeHandler handler, options);

            bool ok = await reporter.ReportAsync("X", "msg", null, "error");

            Assert.False(ok);
            Assert.Null(handler.LastRequest);
        }

        [Fact]
        public async Task ReportAsync_returns_false_instead_of_throwing_on_network_error()
        {
            var reporter = Build(out _, Enabled(), _ => throw new HttpRequestException("Kein DNS"));

            bool ok = await reporter.ReportAsync("X", "msg", null, "error");

            Assert.False(ok);
        }

        [Fact]
        public async Task ReportAsync_returns_false_on_http_error_status()
        {
            var reporter = Build(out _, Enabled(),
                _ => new HttpResponseMessage(HttpStatusCode.Unauthorized));

            bool ok = await reporter.ReportAsync("X", "msg", null, "error");

            Assert.False(ok);
        }

        // ----- Secrets entfernen -----

        [Fact]
        public async Task ReportAsync_redacts_secrets_from_message_and_stack_trace()
        {
            var reporter = Build(out FakeHandler handler, Enabled());

            await reporter.ReportAsync("X",
                "Wallet 0xABCDEF0123456789 kaputt, token enc:v1:ZZZZYYYYXXXX",
                "at Foo(0xDEADBEEFCAFEBABE1234)\nat Bar(enc:v1:AAAABBBBCCCC)",
                "error");

            using var doc = JsonDocument.Parse(handler.LastBody!);
            string message = doc.RootElement.GetProperty("message").GetString()!;
            string stack = doc.RootElement.GetProperty("stack_trace").GetString()!;

            Assert.DoesNotContain("ABCDEF0123456789", message);
            Assert.DoesNotContain("ZZZZYYYY", message);
            Assert.DoesNotContain("DEADBEEFCAFEBABE", stack);
            Assert.DoesNotContain("AAAABBBBCCCC", stack);
        }

        [Fact]
        public async Task ReportAsync_keeps_newlines_in_stack_trace()
        {
            // Anders als die kurze Heartbeat-Meldung darf ein Stacktrace mehrzeilig bleiben - sonst
            // ist er zum Debuggen wertlos.
            var reporter = Build(out FakeHandler handler, Enabled());

            await reporter.ReportAsync("X", "msg", "at Foo()\nat Bar()\nat Baz()", "error");

            using var doc = JsonDocument.Parse(handler.LastBody!);
            Assert.Contains("\n", doc.RootElement.GetProperty("stack_trace").GetString());
        }

        [Fact]
        public async Task ReportAsync_truncates_very_long_stack_traces()
        {
            var reporter = Build(out FakeHandler handler, Enabled());

            await reporter.ReportAsync("X", "msg", new string('a', 10_000), "error");

            using var doc = JsonDocument.Parse(handler.LastBody!);
            Assert.True(doc.RootElement.GetProperty("stack_trace").GetString()!.Length <= 4000);
        }

        // ----- Ratenbegrenzung -----

        [Fact]
        public async Task ReportAsync_stops_sending_after_local_rate_limit_is_hit()
        {
            var reporter = Build(out FakeHandler handler, Enabled());

            int sent = 0;
            for (int i = 0; i < 25; i++)
            {
                if (await reporter.ReportAsync("X", $"msg {i}", null, "error")) sent++;
            }

            // Eigene Grenze liegt bei 20/Minute - deutlich unter dem Serverlimit von 60/Minute/IP.
            Assert.Equal(20, sent);
            Assert.Equal(20, handler.RequestCount);
        }

        // ----- Quellen: App-Log -----

        [Fact]
        public void OnAppLog_reports_error_level_but_ignores_warning_and_info()
        {
            var reporter = Build(out FakeHandler handler, Enabled());
            var logger = new TerminalLogger();

            reporter.OnAppLog(logger, new LogMessageEventArgs("nur eine Warnung", LogLevel.Warning));
            reporter.OnAppLog(logger, new LogMessageEventArgs("nur Info", LogLevel.Info));
            Assert.Null(handler.LastRequest);

            reporter.OnAppLog(logger, new LogMessageEventArgs("echter Fehler", LogLevel.Error));

            // OnAppLog ist Fire-and-forget (synchroner Aufrufer darf nicht blockieren).
            Assert.True(SpinWait.SpinUntil(() => handler.LastRequest != null, TimeSpan.FromSeconds(2)));
        }

        // ----- Quellen: unbeobachtete Task-Exception -----

        [Fact]
        public void OnUnobservedTaskException_reports_as_warning_and_marks_observed()
        {
            var reporter = Build(out FakeHandler handler, Enabled());
            var args = new UnobservedTaskExceptionEventArgs(
                new AggregateException(new InvalidOperationException("Hintergrund kaputt")));

            reporter.OnUnobservedTaskException(null, args);

            Assert.True(SpinWait.SpinUntil(() => handler.LastRequest != null, TimeSpan.FromSeconds(2)));
            using var doc = JsonDocument.Parse(handler.LastBody!);
            Assert.Equal("warning", doc.RootElement.GetProperty("level").GetString());
            Assert.True(args.Observed);
        }

        // ----- Quellen: AppDomain.UnhandledException -----

        [Fact]
        public void OnUnhandledException_reports_synchronously_as_fatal()
        {
            var reporter = Build(out FakeHandler handler, Enabled());
            var args = new UnhandledExceptionEventArgs(new InvalidOperationException("Absturz"), isTerminating: true);

            // Läuft synchron (der Prozess hat keine Zeit mehr für Fire-and-forget) - nach der
            // Rückkehr muss die Anfrage also bereits abgesetzt sein.
            reporter.OnUnhandledException(new object(), args);

            Assert.NotNull(handler.LastRequest);
            using var doc = JsonDocument.Parse(handler.LastBody!);
            Assert.Equal("fatal", doc.RootElement.GetProperty("level").GetString());
            Assert.Contains("Absturz", doc.RootElement.GetProperty("message").GetString());
        }

        [Fact]
        public void OnUnhandledException_never_throws_even_if_sending_fails()
        {
            var reporter = Build(out _, Enabled(), _ => throw new HttpRequestException("Kein Netz"));
            var args = new UnhandledExceptionEventArgs(new InvalidOperationException("Absturz"), isTerminating: true);

            var ex = Record.Exception(() => reporter.OnUnhandledException(new object(), args));

            Assert.Null(ex);
        }

        // ----- Helfer -----

        private static DeploymentcenterOptions Enabled() => new()
        {
            ErrorReportingEnabled = true,
            BaseUrl = "https://dc.example.de",
            Token = "secret-token",
            ProjectSlug = "polytrader"
        };

        private static DeploymentcenterErrorReporter Build(
            out FakeHandler handler,
            DeploymentcenterOptions options,
            Func<HttpRequestMessage, HttpResponseMessage>? responder = null)
        {
            handler = new FakeHandler(responder ?? (_ => new HttpResponseMessage(HttpStatusCode.OK)));
            return new DeploymentcenterErrorReporter(new TerminalLogger(), options, new HttpClient(handler));
        }

        private sealed class FakeHandler : HttpMessageHandler
        {
            private readonly Func<HttpRequestMessage, HttpResponseMessage> _responder;
            public FakeHandler(Func<HttpRequestMessage, HttpResponseMessage> responder) => _responder = responder;

            public HttpRequestMessage? LastRequest { get; private set; }
            public string? LastBody { get; private set; }
            public int RequestCount { get; private set; }

            protected override async Task<HttpResponseMessage> SendAsync(
                HttpRequestMessage request, CancellationToken cancellationToken)
            {
                LastRequest = request;
                RequestCount++;
                LastBody = request.Content != null
                    ? await request.Content.ReadAsStringAsync(cancellationToken)
                    : string.Empty;
                return _responder(request);
            }
        }
    }
}
