using GxMcp.Gateway;
using Newtonsoft.Json.Linq;
using Xunit;

namespace GxMcp.Gateway.Tests
{
    // Friction 2026-05-22 #63: every error envelope should carry a structured
    // "what to do next" hint. Pre-existing suggested_next_step is preserved;
    // otherwise McpRouter.AttachSuggestedNextStep synthesizes one from the
    // code / message pattern. Covers the four cases the spec called out.
    public class SuggestedNextStepTests
    {
        [Fact]
        public void PatchNoMatch_PointsAtNearMatchInspection()
        {
            var err = JObject.Parse(@"{""code"":""patch_no_match"",""message"":""Context not found.""}");
            var hint = McpRouter.AttachSuggestedNextStep(err);
            Assert.NotNull(hint);
            Assert.Equal("inspect_near_match", hint["action"]!.ToString());
            Assert.Contains("nearMatches", hint["hint"]!.ToString());
        }

        [Fact]
        public void PatchAmbiguous_AlsoRoutesToNearMatchHint()
        {
            // 'Ambiguous patch: Found N exact matches' is the sibling of NoMatch
            // (matching produced too many hits, not zero). The same near-match
            // / replaceAll guidance applies.
            var err = JObject.Parse(@"{""code"":""Error"",""message"":""Ambiguous patch: Found 3 exact matches, but expected 1.""}");
            var hint = McpRouter.AttachSuggestedNextStep(err);
            Assert.NotNull(hint);
            Assert.Equal("inspect_near_match", hint["action"]!.ToString());
        }

        [Fact]
        public void VisualWriteFailure_PointsAtLayoutGotchaScanner()
        {
            var err = JObject.Parse(@"{""status"":""Error"",""error"":""Invalid visual XML: unclosed gxButton tag""}");
            var hint = McpRouter.AttachSuggestedNextStep(err);
            Assert.NotNull(hint);
            Assert.Equal("run_layout_gotcha_scanner", hint["action"]!.ToString());
            Assert.Contains("layoutGotchas", hint["hint"]!.ToString());
        }

        [Fact]
        public void KbAmbiguous_PointsAtKbParameter()
        {
            var err = JObject.Parse(@"{""code"":""KB_AMBIGUOUS"",""message"":""KB_AMBIGUOUS: multiple KBs open""}");
            var hint = McpRouter.AttachSuggestedNextStep(err);
            Assert.NotNull(hint);
            Assert.Equal("specify_kb", hint["action"]!.ToString());
            Assert.Contains("kb=", hint["hint"]!.ToString());
        }

        [Fact]
        public void Spc0150_PointsAtExtractToProcedureRecipe()
        {
            var err = JObject.Parse(@"{""code"":""Error"",""error"":""spc0150 — Attribute cannot be assigned in this context""}");
            var hint = McpRouter.AttachSuggestedNextStep(err);
            Assert.NotNull(hint);
            Assert.Equal("recipe_extract_to_procedure", hint["action"]!.ToString());
            Assert.Equal("extract_to_procedure", hint["recipe"]!.ToString());
        }

        [Fact]
        public void UnknownError_ReturnsNull()
        {
            // No registered pattern → null. Callers fall back to message/hint.
            var err = JObject.Parse(@"{""code"":""SomeNewErrorNobodyRegistered"",""message"":""..""}");
            var hint = McpRouter.AttachSuggestedNextStep(err);
            Assert.Null(hint);
        }

        [Fact]
        public void TrimErrorEnvelope_PreservesPreExistingSuggestedNextStep()
        {
            // Worker-side payloads (e.g. write_not_persisted) carry their own
            // suggested_next_step. Trim must not clobber it.
            var err = JObject.Parse(@"{""code"":""write_not_persisted"",""message"":""..."",""suggested_next_step"":""Retry the same patch.""}");
            var trimmed = McpRouter.TrimErrorEnvelope(err, verbose: false);
            Assert.NotNull(trimmed["suggested_next_step"]);
            Assert.Equal("Retry the same patch.", trimmed["suggested_next_step"]!.ToString());
        }

        [Fact]
        public void TrimErrorEnvelope_SynthesizesHint_WhenNoneOnPayload()
        {
            var err = JObject.Parse(@"{""code"":""patch_no_match"",""message"":""Context not found.""}");
            var trimmed = McpRouter.TrimErrorEnvelope(err, verbose: false);
            Assert.NotNull(trimmed["suggested_next_step"]);
        }
    }
}
