using System.Net;
using System.Text;
using System.Text.Json;
using GerbilManagerWebAPI.SaleAd;
using Microsoft.Extensions.Options;

namespace GerbilManager.Tests
{
    /// <summary>
    /// FEAT-12a: sale-ad backend — prompt assembly, the 503-unconfigured path,
    /// the OpenAI-compatible wire handling against a stubbed provider, and the
    /// provider config matrix (Gemini / Groq / Mistral / Ollama base URLs).
    /// </summary>
    public class SaleAdTests
    {
        private static SaleAdRequest SampleRequest() => new(
            Animals:
            [
                new SaleAdAnimal("Balu", "CP-Agouti", "2024-03-12", "ruhig, nimmt Leckerlis aus der Hand"),
                new SaleAdAnimal("Benny", "Schwarz Schecke", "2024-03-12", "neugieriger Entdecker"),
            ],
            StatusLine: "FREI",
            Hints: "bitte die Bruder-Dynamik betonen");

        // ── Prompt assembly ────────────────────────────────────────────────

        [Fact]
        public void UserPrompt_enthält_Tiere_Status_und_Hinweise()
        {
            var prompt = SaleAdPromptBuilder.BuildUserPrompt(SampleRequest());

            Assert.Contains("Status-Zeile: FREI", prompt);
            Assert.Contains("Name: Balu", prompt);
            Assert.Contains("Farbschlag: CP-Agouti", prompt);
            Assert.Contains("Name: Benny", prompt);
            Assert.Contains("ruhig, nimmt Leckerlis aus der Hand", prompt);
            Assert.Contains("Wünsche/Hinweise: bitte die Bruder-Dynamik betonen", prompt);
        }

        [Fact]
        public void UserPrompt_formatiert_Geburtsdatum_deutsch()
        {
            var prompt = SaleAdPromptBuilder.BuildUserPrompt(SampleRequest());
            Assert.Contains("geboren am 12.03.2024", prompt);
            Assert.DoesNotContain("2024-03-12", prompt);
        }

        [Fact]
        public void UserPrompt_lässt_fehlende_Angaben_weg()
        {
            var request = new SaleAdRequest(
                [new SaleAdAnimal("Mysti", null, null, null)], "RESERVIERT", "");
            var prompt = SaleAdPromptBuilder.BuildUserPrompt(request);

            Assert.Contains("Name: Mysti", prompt);
            Assert.DoesNotContain("Farbschlag:", prompt);
            Assert.DoesNotContain("geboren am", prompt);
            Assert.DoesNotContain("Notizen:", prompt);
            Assert.DoesNotContain("Wünsche/Hinweise:", prompt);
        }

        [Fact]
        public void SystemPrompt_enthält_Stilregeln_und_beide_Beispiele()
        {
            var prompt = SaleAdPromptBuilder.BuildSystemPrompt();

            Assert.Contains("Zucht der kleinen Chaoten", prompt);
            Assert.Contains("NIEMALS Preise", prompt);
            Assert.Contains("KEINE Fakten erfinden", prompt);
            // few-shot: verbatim listings from kleine-chaoten.jimdofree.com
            Assert.Contains("Status: LOCKER RESERVIERT LEONIE", prompt);
            Assert.Contains("Status: FREI", prompt);
            Assert.Contains("Großer und kleiner Bruder Dynamik", prompt);
        }

        // ── FEAT-14c: Charakterbogen (Traits + Notiz) im Prompt ────────────

        [Fact]
        public void UserPrompt_enthält_Traits_und_Charakternotiz()
        {
            var request = new SaleAdRequest(
                [new SaleAdAnimal("Balu", "CP-Agouti", "2024-03-12", "alte Verwaltungsnotiz",
                    Traits: ["zutraulich", "buddelt gern"],
                    CharacterNote: "klettert abends auf die Hand")],
                "FREI", "");
            var prompt = SaleAdPromptBuilder.BuildUserPrompt(request);

            Assert.Contains("Charakter: zutraulich, buddelt gern", prompt);
            Assert.Contains("Charakter-Notiz: klettert abends auf die Hand", prompt);
        }

        [Fact]
        public void UserPrompt_bevorzugt_Charakterbogen_vor_generischen_Notizen()
        {
            // Traits vorhanden -> die generische Notiz (oft Verwaltungs-Info)
            // gehört NICHT in den Prompt.
            var request = new SaleAdRequest(
                [new SaleAdAnimal("Balu", null, null, "Käfig wurde am 3.5. gereinigt",
                    Traits: ["neugierig"])],
                "FREI", "");
            var prompt = SaleAdPromptBuilder.BuildUserPrompt(request);

            Assert.Contains("Charakter: neugierig", prompt);
            Assert.DoesNotContain("Notizen:", prompt);
            Assert.DoesNotContain("Käfig wurde", prompt);
        }

        [Fact]
        public void UserPrompt_fällt_ohne_Charakterbogen_auf_Notizen_zurück()
        {
            // Kein Charakterbogen (null bzw. leer) -> Notizen wie bisher.
            var request = new SaleAdRequest(
                [
                    new SaleAdAnimal("Mysti", null, null, "sehr verschmust", Traits: null),
                    new SaleAdAnimal("Bo", null, null, "mag Kolbenhirse", Traits: []),
                ],
                "FREI", "");
            var prompt = SaleAdPromptBuilder.BuildUserPrompt(request);

            Assert.Contains("Notizen: sehr verschmust", prompt);
            Assert.Contains("Notizen: mag Kolbenhirse", prompt);
            Assert.DoesNotContain("Charakter:", prompt);
        }

        [Fact]
        public void SystemPrompt_verlangt_fließende_Prosa_statt_Stichwortliste()
        {
            var prompt = SaleAdPromptBuilder.BuildSystemPrompt();
            Assert.Contains("Charakter-Eigenschaften", prompt);
            Assert.Contains("niemals als Aufzählung", prompt);
        }

        [Theory]
        [InlineData("2024-03-12", "12.03.2024")]
        [InlineData("kaputt", "kaputt")] // unparsebar -> unverändert durchreichen
        public void FormatGermanDate_konvertiert_ISO_nach_TT_MM_JJJJ(string input, string expected)
            => Assert.Equal(expected, SaleAdPromptBuilder.FormatGermanDate(input));

        // ── 503: unkonfiguriert ────────────────────────────────────────────

        [Theory]
        [InlineData(null, "key", "model")]
        [InlineData("https://api.example.com/v1", null, "model")]
        [InlineData("https://api.example.com/v1", "key", null)]
        [InlineData(null, null, null)]
        public async Task GenerateAsync_meldet_NotConfigured_wenn_eine_Einstellung_fehlt(
            string? baseUrl, string? apiKey, string? model)
        {
            var service = CreateService(baseUrl, apiKey, model,
                new StubHandler(_ => throw new InvalidOperationException("darf nicht aufgerufen werden")));

            var result = await service.GenerateAsync(SampleRequest());

            Assert.Equal(SaleAdStatus.NotConfigured, result.Status);
            Assert.Null(result.Text);
        }

        // ── Fake-Server: OpenAI-kompatible Antwort wird geparst ────────────

        [Fact]
        public async Task GenerateAsync_parst_die_Chat_Completion_des_Stubs()
        {
            var handler = new StubHandler(_ => Canned("Status: FREI\n\nWunderbares Duo …"));
            var service = CreateService("https://api.groq.com/openai/v1", "k", "llama-3.3-70b", handler);

            var result = await service.GenerateAsync(SampleRequest());

            Assert.Equal(SaleAdStatus.Ok, result.Status);
            Assert.Equal("Status: FREI\n\nWunderbares Duo …", result.Text);
        }

        [Fact]
        public async Task GenerateAsync_sendet_Modell_Messages_und_Bearer_Key()
        {
            var handler = new StubHandler(_ => Canned("ok"));
            var service = CreateService("https://api.example.com/v1", "geheim", "test-model", handler);

            await service.GenerateAsync(SampleRequest());

            Assert.NotNull(handler.LastRequest);
            Assert.Equal("Bearer", handler.LastRequest!.Headers.Authorization?.Scheme);
            Assert.Equal("geheim", handler.LastRequest.Headers.Authorization?.Parameter);

            using var body = JsonDocument.Parse(handler.LastRequestBody!);
            Assert.Equal("test-model", body.RootElement.GetProperty("model").GetString());
            var messages = body.RootElement.GetProperty("messages");
            Assert.Equal(2, messages.GetArrayLength());
            Assert.Equal("system", messages[0].GetProperty("role").GetString());
            Assert.Equal("user", messages[1].GetProperty("role").GetString());
        }

        [Fact]
        public async Task GenerateAsync_meldet_UpstreamError_bei_Provider_HTTP_Fehler()
        {
            var handler = new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.TooManyRequests)
            {
                Content = new StringContent("rate limited"),
            });
            var service = CreateService("https://api.example.com/v1", "k", "m", handler);

            var result = await service.GenerateAsync(SampleRequest());

            Assert.Equal(SaleAdStatus.UpstreamError, result.Status);
        }

        [Fact]
        public async Task GenerateAsync_meldet_UpstreamError_bei_leerer_Antwort()
        {
            var handler = new StubHandler(_ => Canned(""));
            var service = CreateService("https://api.example.com/v1", "k", "m", handler);

            var result = await service.GenerateAsync(SampleRequest());

            Assert.Equal(SaleAdStatus.UpstreamError, result.Status);
        }

        // ── Provider-Konfigurationsmatrix: BaseUrl -> /chat/completions ────

        [Theory]
        [InlineData("https://generativelanguage.googleapis.com/v1beta/openai",
                    "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions")] // Gemini
        [InlineData("https://api.groq.com/openai/v1",
                    "https://api.groq.com/openai/v1/chat/completions")] // Groq
        [InlineData("https://api.mistral.ai/v1",
                    "https://api.mistral.ai/v1/chat/completions")] // Mistral
        [InlineData("http://truenas:11434/v1",
                    "http://truenas:11434/v1/chat/completions")] // Ollama (lokal)
        [InlineData("https://api.groq.com/openai/v1/",
                    "https://api.groq.com/openai/v1/chat/completions")] // trailing slash
        public async Task Konfigurationsmatrix_baut_die_richtige_Completions_URL(
            string baseUrl, string expectedUri)
        {
            Assert.Equal(new Uri(expectedUri), SaleAdService.BuildCompletionsUri(baseUrl));

            // und der Stub sieht den Aufruf tatsächlich auf dieser URL
            var handler = new StubHandler(_ => Canned("ok"));
            var service = CreateService(baseUrl, "k", "m", handler);
            await service.GenerateAsync(SampleRequest());
            Assert.Equal(new Uri(expectedUri), handler.LastRequest!.RequestUri);
        }

        // ── Helfer ─────────────────────────────────────────────────────────

        private static SaleAdService CreateService(
            string? baseUrl, string? apiKey, string? model, StubHandler handler)
        {
            var options = Options.Create(new AiOptions { BaseUrl = baseUrl, ApiKey = apiKey, Model = model });
            return new SaleAdService(new HttpClient(handler), options);
        }

        private static HttpResponseMessage Canned(string content)
        {
            var completion = new
            {
                choices = new[] { new { message = new { role = "assistant", content } } },
            };
            return new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(JsonSerializer.Serialize(completion),
                    Encoding.UTF8, "application/json"),
            };
        }

        /// <summary>HttpMessageHandler-Stub, der Request + Body zur Inspektion festhält.</summary>
        private sealed class StubHandler(Func<HttpRequestMessage, HttpResponseMessage> respond)
            : HttpMessageHandler
        {
            public HttpRequestMessage? LastRequest { get; private set; }
            public string? LastRequestBody { get; private set; }

            protected override async Task<HttpResponseMessage> SendAsync(
                HttpRequestMessage request, CancellationToken cancellationToken)
            {
                LastRequest = request;
                LastRequestBody = request.Content is null
                    ? null
                    : await request.Content.ReadAsStringAsync(cancellationToken);
                return respond(request);
            }
        }
    }
}
