using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
using Xunit;

namespace PolyTrader.Tests
{
    /// <summary>
    /// Sicherheitsnetz für den Watchdog-Heartbeat (B.1). Die Tests fixieren die drei Eigenschaften,
    /// die im Review der Fremdprojekte aufgefallen sind:
    /// 1. TLS-Zwang (der mitgelieferte Watchdog-Testclient deaktiviert die Zertifikatsprüfung),
    /// 2. Feldname "interval" – die Watchdog-API ignoriert "intervalSeconds" still,
    /// 3. ein nicht erreichbarer Watchdog darf niemals eine Ausnahme nach außen geben.
    /// </summary>
    public class WatchdogHeartbeatTests
    {
        // ----- TLS-Zwang / URL-Aufbau -----

        [Theory]
        [InlineData("https://watchdog.example.de")]
        [InlineData("https://watchdog.example.de/")]
        [InlineData("http://localhost:8080")]
        [InlineData("http://127.0.0.1")]
        public void TryBuildEndpoint_accepts_https_and_local_http(string baseUrl)
        {
            bool ok = WatchdogHeartbeatService.TryBuildEndpoint(baseUrl, WatchdogHeartbeatService.PingPath, out Uri? uri, out string problem);

            Assert.True(ok, problem);
            Assert.NotNull(uri);
            Assert.EndsWith("/api/watchdog/v1/ping", uri!.AbsoluteUri);
        }

        [Fact]
        public void TryBuildEndpoint_rejects_plain_http_to_remote_host()
        {
            // Über http:// ginge der Agent-Token im Klartext über das Netz.
            bool ok = WatchdogHeartbeatService.TryBuildEndpoint(
                "http://watchdog.example.de", WatchdogHeartbeatService.PingPath, out Uri? uri, out string problem);

            Assert.False(ok);
            Assert.Null(uri);
            Assert.Contains("HTTPS", problem);
        }

        [Theory]
        [InlineData("")]
        [InlineData("   ")]
        [InlineData("watchdog.example.de")]      // ohne Schema
        [InlineData("ftp://watchdog.example.de")]
        public void TryBuildEndpoint_rejects_invalid_urls(string baseUrl)
        {
            bool ok = WatchdogHeartbeatService.TryBuildEndpoint(baseUrl, WatchdogHeartbeatService.PingPath, out _, out string problem);

            Assert.False(ok);
            Assert.NotEmpty(problem);
        }

        [Fact]
        public void TryBuildEndpoint_keeps_base_path_of_subdirectory_installs()
        {
            bool ok = WatchdogHeartbeatService.TryBuildEndpoint(
                "https://example.de/watchdog", WatchdogHeartbeatService.PingPath, out Uri? uri, out _);

            Assert.True(ok);
            Assert.Equal("https://example.de/watchdog/api/watchdog/v1/ping", uri!.AbsoluteUri);
        }

        // ----- Konfigurationsprüfung -----

        [Fact]
        public void GetConfigurationProblem_reports_missing_url_token_and_source()
        {
            Assert.Contains("URL", Problem(s => s.WatchdogUrl = ""));
            Assert.Contains("Token", Problem(s =>
            {
                s.WatchdogUrl = "https://wd.example.de";
                s.WatchdogToken = "";
            }));
            Assert.Contains("Source", Problem(s =>
            {
                s.WatchdogUrl = "https://wd.example.de";
                s.WatchdogToken = "tok";
                s.WatchdogSource = "";
            }));
        }

        [Fact]
        public void GetConfigurationProblem_returns_null_when_ready()
        {
            var service = Build(out _, Valid());
            Assert.Null(service.GetConfigurationProblem());
        }

        // ----- Payload -----

        [Fact]
        public async Task SendHeartbeatAsync_posts_expected_payload_and_auth_header()
        {
            var settings = Valid();
            settings.WatchdogSource = "polytrader";
            settings.WatchdogInstance = "prod-1";
            settings.WatchdogIntervalSeconds = 45;

            var service = Build(out FakeHandler handler, settings);

            var result = await service.SendHeartbeatAsync("ok", "Testlauf");

            Assert.True(result.Success, result.Detail);
            Assert.Equal(HttpMethod.Post, handler.LastRequest!.Method);
            Assert.Equal("https://wd.example.de/api/watchdog/v1/ping", handler.LastRequest.RequestUri!.AbsoluteUri);
            Assert.Equal("Bearer", handler.LastRequest.Headers.Authorization!.Scheme);
            Assert.Equal("secret-token", handler.LastRequest.Headers.Authorization!.Parameter);
            Assert.False(handler.LastRequest.Headers.Contains("X-Watchdog-Key"));

            using var doc = JsonDocument.Parse(handler.LastBody!);
            var root = doc.RootElement;
            Assert.Equal("polytrader", root.GetProperty("source").GetString());
            Assert.Equal("prod-1", root.GetProperty("instance").GetString());
            Assert.Equal("heartbeat", root.GetProperty("type").GetString());
            Assert.Equal("ok", root.GetProperty("status").GetString());
            Assert.Equal("Testlauf", root.GetProperty("message").GetString());

            // Entscheidend: die Watchdog-API liest "interval" und ignoriert "intervalSeconds" still.
            Assert.Equal(45, root.GetProperty("interval").GetInt32());
            Assert.False(root.TryGetProperty("intervalSeconds", out _));

            // D-1: Version aus BuildInfo (Directory.Build.props), nicht mehr aus der Assembly-Version.
            Assert.Equal(PolyTrader.Core.BuildInfo.Version, root.GetProperty("version").GetString());
            Assert.False(string.IsNullOrWhiteSpace(root.GetProperty("os").GetString()));
        }

        [Fact]
        public async Task SendHeartbeatAsync_includes_registered_checks()
        {
            var service = Build(out FakeHandler handler, Valid());
            service.RegisterCheck("db", () => (true, null));
            service.RegisterCheck("market_feed", () => (false, "Letzter Tick vor 14 min"));

            await service.SendHeartbeatAsync();

            using var doc = JsonDocument.Parse(handler.LastBody!);
            var checks = doc.RootElement.GetProperty("checks");
            Assert.True(checks.GetProperty("db").GetProperty("ok").GetBoolean());
            Assert.False(checks.GetProperty("market_feed").GetProperty("ok").GetBoolean());
            Assert.Equal("Letzter Tick vor 14 min", checks.GetProperty("market_feed").GetProperty("message").GetString());
        }

        [Fact]
        public async Task SendHeartbeatAsync_omits_checks_when_none_registered()
        {
            var service = Build(out FakeHandler handler, Valid());
            await service.SendHeartbeatAsync();

            using var doc = JsonDocument.Parse(handler.LastBody!);
            Assert.True(doc.RootElement.TryGetProperty("checks", out var checksProp));
            Assert.Equal(JsonValueKind.Null, checksProp.ValueKind);
        }

        [Fact]
        public async Task SendHeartbeatAsync_check_failure_reports_itself_instead_of_throwing()
        {
            var service = Build(out FakeHandler handler, Valid());
            service.RegisterCheck("boom", () => throw new InvalidOperationException("kaputt"));

            var result = await service.SendHeartbeatAsync();

            Assert.True(result.Success, result.Detail);   // die werfende Prüfung darf den Versand nicht verhindern
            using var doc = JsonDocument.Parse(handler.LastBody!);
            var boom = doc.RootElement.GetProperty("checks").GetProperty("boom");
            Assert.False(boom.GetProperty("ok").GetBoolean());
            Assert.Contains("kaputt", boom.GetProperty("message").GetString());
        }

        [Fact]
        public async Task UnregisterCheck_removes_a_previously_registered_check()
        {
            var service = Build(out FakeHandler handler, Valid());
            service.RegisterCheck("temp", () => (true, null));
            service.UnregisterCheck("temp");

            await service.SendHeartbeatAsync();

            using var doc = JsonDocument.Parse(handler.LastBody!);
            Assert.Equal(JsonValueKind.Null, doc.RootElement.GetProperty("checks").ValueKind);
        }

        [Fact]
        public async Task SendHeartbeatAsync_clamps_interval_to_minimum()
        {
            var settings = Valid();
            settings.WatchdogIntervalSeconds = 1;      // absurd niedrig -> würde den Watchdog fluten

            var service = Build(out FakeHandler handler, settings);
            await service.SendHeartbeatAsync();

            using var doc = JsonDocument.Parse(handler.LastBody!);
            Assert.Equal(10, doc.RootElement.GetProperty("interval").GetInt32());
        }

        [Fact]
        public async Task SendHeartbeatAsync_falls_back_to_default_instance_when_blank()
        {
            var settings = Valid();
            settings.WatchdogInstance = "  ";

            var service = Build(out FakeHandler handler, settings);
            await service.SendHeartbeatAsync();

            using var doc = JsonDocument.Parse(handler.LastBody!);
            Assert.Equal("default", doc.RootElement.GetProperty("instance").GetString());
        }

        // ----- Robustheit: ein toter Watchdog darf PolyTrader nicht beeinträchtigen -----

        [Fact]
        public async Task SendHeartbeatAsync_returns_failure_instead_of_throwing_on_network_error()
        {
            var service = Build(out _, Valid(), _ => throw new HttpRequestException("Kein DNS"));

            var result = await service.SendHeartbeatAsync();

            Assert.False(result.Success);
            Assert.Contains("Kein DNS", result.Detail);
        }

        [Fact]
        public async Task SendHeartbeatAsync_reports_http_error_status_as_failure()
        {
            var service = Build(out _, Valid(),
                _ => new HttpResponseMessage(HttpStatusCode.Unauthorized) { ReasonPhrase = "Unauthorized" });

            var result = await service.SendHeartbeatAsync();

            Assert.False(result.Success);
            Assert.Contains("401", result.Detail);
        }

        [Fact]
        public async Task SendHeartbeatAsync_does_not_send_when_misconfigured()
        {
            var settings = Valid();
            settings.WatchdogUrl = "http://watchdog.example.de";   // kein TLS

            var service = Build(out FakeHandler handler, settings);
            var result = await service.SendHeartbeatAsync();

            Assert.False(result.Success);
            Assert.Null(handler.LastRequest);   // gar nicht erst gesendet
        }

        [Fact]
        public async Task ExecuteAsync_returns_quietly_when_disabled()
        {
            var settings = Valid();
            settings.WatchdogEnabled = false;

            var service = Build(out FakeHandler handler, settings);

            // StartAsync eines BackgroundService darf bei deaktivierter Konfiguration nicht werfen
            // und nichts senden.
            await service.StartAsync(CancellationToken.None);
            await service.StopAsync(CancellationToken.None);

            Assert.Null(handler.LastRequest);
        }

        [Fact]
        public async Task ExecuteAsync_does_not_throw_when_enabled_but_misconfigured()
        {
            var settings = Valid();
            settings.WatchdogEnabled = true;
            settings.WatchdogToken = "";        // Fehlkonfiguration

            var service = Build(out FakeHandler handler, settings);

            await service.StartAsync(CancellationToken.None);
            await service.StopAsync(CancellationToken.None);

            Assert.Null(handler.LastRequest);
        }

        [Fact]
        public async Task Running_service_starts_sending_after_being_enabled_without_restart()
        {
            // Der Nutzer schaltet den Heartbeat im Settings-Fenster ein: der laufende Dienst muss
            // das übernehmen, ohne dass PolyTrader neu gestartet werden muss.
            var settings = Valid();
            settings.WatchdogEnabled = false;

            var service = Build(out FakeHandler handler, settings);
            await service.StartAsync(CancellationToken.None);
            try
            {
                Assert.Null(handler.LastRequest);         // deaktiviert -> nichts gesendet

                settings.WatchdogEnabled = true;
                service.ReloadSettings();                 // genau das macht der Speichern-Button

                Assert.True(await WaitForRequestAsync(handler),
                    "Nach dem Aktivieren wurde kein Heartbeat gesendet.");
            }
            finally
            {
                await service.StopAsync(CancellationToken.None);
            }
        }

        [Fact]
        public async Task Running_service_stops_sending_after_being_disabled_without_restart()
        {
            var settings = Valid();
            var service = Build(out FakeHandler handler, settings);

            await service.StartAsync(CancellationToken.None);
            try
            {
                Assert.True(await WaitForRequestAsync(handler), "Es wurde kein initialer Heartbeat gesendet.");

                settings.WatchdogEnabled = false;
                service.ReloadSettings();
                handler.Reset();

                // Der Takt liegt bei 60 s; entscheidend ist, dass nach dem Ausschalten nichts mehr kommt.
                await Task.Delay(TimeSpan.FromSeconds(2));
                Assert.Null(handler.LastRequest);
            }
            finally
            {
                await service.StopAsync(CancellationToken.None);
            }
        }

        [Fact]
        public void ReloadSettings_picks_up_changed_values()
        {
            var settings = Valid();
            settings.WatchdogUrl = "http://watchdog.example.de";   // zunächst ungültig

            var service = new WatchdogHeartbeatService(new TerminalLogger(),
                new HttpClient(new FakeHandler(_ => new HttpResponseMessage(HttpStatusCode.OK))),
                () => settings);

            Assert.NotNull(service.GetConfigurationProblem());

            settings.WatchdogUrl = "https://watchdog.example.de";
            service.ReloadSettings();

            Assert.Null(service.GetConfigurationProblem());
        }

        // ----- B.2: Status aus dem Log -----

        [Fact]
        public void DeriveStatus_is_ok_without_errors()
        {
            var logger = new TerminalLogger();
            var service = new WatchdogHeartbeatService(logger,
                new HttpClient(new FakeHandler(_ => new HttpResponseMessage(HttpStatusCode.OK))), () => Valid());

            Assert.Equal("ok", service.DeriveStatus().status);
        }

        [Fact]
        public void DeriveStatus_reflects_logged_error_and_redacts_secrets()
        {
            var logger = new TerminalLogger();
            var service = new WatchdogHeartbeatService(logger,
                new HttpClient(new FakeHandler(_ => new HttpResponseMessage(HttpStatusCode.OK))), () => Valid());

            logger.Error("DB-Verbindung tot; wallet 0xDEADBEEFCAFEBABE1234 token enc:v1:AAAABBBBCCCC");

            var (status, message) = service.DeriveStatus();
            Assert.Equal("error", status);
            Assert.Contains("DB-Verbindung tot", message);
            // Secrets müssen entschärft sein (kein Wallet-Hex / kein enc-Token durchreichen).
            Assert.DoesNotContain("DEADBEEFCAFEBABE", message);
            Assert.DoesNotContain("AAAABBBBCCCC", message);
        }

        [Fact]
        public void DeriveStatus_ignores_warning_and_info()
        {
            var logger = new TerminalLogger();
            var service = new WatchdogHeartbeatService(logger,
                new HttpClient(new FakeHandler(_ => new HttpResponseMessage(HttpStatusCode.OK))), () => Valid());

            logger.Warning("nur eine Warnung");
            logger.Info("nur Info");

            // Warnungen/Infos eskalieren den Monitor bewusst nicht.
            Assert.Equal("ok", service.DeriveStatus().status);
        }

        [Theory]
        [InlineData("0xABCDEF0123456789 kaputt", "DEF012")]
        [InlineData("secret enc:v1:ZZZZYYYYXXXX weg", "ZZZZYYYY")]
        public void Sanitize_removes_secret_material(string input, string mustNotContain)
        {
            string cleaned = WatchdogHeartbeatService.Sanitize(input);
            Assert.DoesNotContain(mustNotContain, cleaned);
        }

        [Fact]
        public void Sanitize_truncates_to_200_chars()
        {
            string cleaned = WatchdogHeartbeatService.Sanitize(new string('x', 500));
            Assert.True(cleaned.Length <= 200, $"Länge {cleaned.Length}");
        }

        // ----- B.3: Lifecycle-Events -----

        [Fact]
        public async Task SendEventAsync_posts_kind_and_source_to_event_endpoint()
        {
            var settings = Valid();
            var service = Build(out FakeHandler handler, settings);

            var result = await service.SendEventAsync("started", "info", "Test");

            Assert.True(result.Success, result.Detail);
            Assert.Equal("https://wd.example.de/api/watchdog/v1/event", handler.LastRequest!.RequestUri!.AbsoluteUri);
            Assert.Equal("Bearer", handler.LastRequest.Headers.Authorization!.Scheme);
            using var doc = JsonDocument.Parse(handler.LastBody!);
            Assert.Equal("started", doc.RootElement.GetProperty("kind").GetString());
            Assert.Equal("polytrader", doc.RootElement.GetProperty("source").GetString());
        }

        [Fact]
        public async Task Service_sends_started_then_stopped_graceful_over_its_lifecycle()
        {
            var settings = Valid();
            var service = Build(out FakeHandler handler, settings);

            await service.StartAsync(CancellationToken.None);
            Assert.True(await WaitForBodyContainingAsync(handler, "\"kind\":\"started\""),
                "Es wurde kein 'started'-Event gesendet.");

            await service.StopAsync(CancellationToken.None);
            Assert.True(handler.AnyBody(b => b.Contains("\"kind\":\"stopped_graceful\"")),
                "Beim Herunterfahren wurde kein 'stopped_graceful'-Event gesendet.");
        }

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

        /// <summary>Wartet begrenzt darauf, dass eine Anfrage mit passendem Body abgesetzt wurde.</summary>
        private static async Task<bool> WaitForBodyContainingAsync(FakeHandler handler, string needle, int timeoutMs = 5000)
        {
            for (int waited = 0; waited < timeoutMs; waited += 50)
            {
                if (handler.AnyBody(b => b.Contains(needle))) return true;
                await Task.Delay(50);
            }
            return handler.AnyBody(b => b.Contains(needle));
        }

        /// <summary>Wartet begrenzt darauf, dass der Dienst eine Anfrage abgesetzt hat.</summary>
        private static async Task<bool> WaitForRequestAsync(FakeHandler handler, int timeoutMs = 5000)
        {
            for (int waited = 0; waited < timeoutMs; waited += 50)
            {
                if (handler.LastRequest != null) return true;
                await Task.Delay(50);
            }
            return handler.LastRequest != null;
        }

        private static ServerSettings Valid() => new()
        {
            WatchdogEnabled = true,
            WatchdogUrl = "https://wd.example.de",
            WatchdogToken = "secret-token",
            WatchdogSource = "polytrader",
            WatchdogInstance = "prod-1",
            WatchdogIntervalSeconds = 60
        };

        private static string Problem(Action<ServerSettings> configure)
        {
            var settings = Valid();
            configure(settings);
            var service = Build(out _, settings);
            return service.GetConfigurationProblem() ?? string.Empty;
        }

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

        /// <summary>Fängt die Anfrage ab, statt sie zu senden.</summary>
        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; }

            private readonly object _gate = new();
            private readonly List<string> _bodies = new();

            /// <summary>Thread-sicherer Prädikat-Check über alle bisher gesendeten Bodies.</summary>
            public bool AnyBody(Func<string, bool> predicate)
            {
                lock (_gate)
                {
                    foreach (var b in _bodies)
                        if (predicate(b)) return true;
                    return false;
                }
            }

            public void Reset()
            {
                LastRequest = null;
                LastBody = null;
                lock (_gate) _bodies.Clear();
            }

            protected override async Task<HttpResponseMessage> SendAsync(
                HttpRequestMessage request, CancellationToken cancellationToken)
            {
                LastRequest = request;
                string body = string.Empty;
                if (request.Content != null)
                {
                    body = await request.Content.ReadAsStringAsync(cancellationToken);
                }
                LastBody = body;
                lock (_gate) _bodies.Add(body);
                return _responder(request);
            }
        }
    }
}
