//
//  Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

package component

import (
	"context"
	"strings"
	"testing"

	"ragflow/internal/agent/audio"
	"ragflow/internal/agent/canvas"
)

// fakeSynthesizer is a programmable audio.Synthesizer for
// integration tests. It records the request and returns the
// canned response / error configured at construction time.
type fakeSynthesizer struct {
	resp *audio.SynthesizeResponse
	err  error
	got  *audio.SynthesizeRequest
}

func (f *fakeSynthesizer) Synthesize(_ context.Context, req audio.SynthesizeRequest) (*audio.SynthesizeResponse, error) {
	if f.got != nil {
		*f.got = req
	}
	return f.resp, f.err
}

// TestMessage_OutputFormatParam: when the DSL declares
// output_format=html, the rendered body is HTML-wrapped.
func TestMessage_OutputFormatParam(t *testing.T) {
	c, _ := NewMessageComponent(map[string]any{
		"text":          "hello",
		"output_format": "html",
	})
	state := canvas.NewCanvasState("r1", "t1")
	ctx := withStateForTest(context.Background(), state)

	out, err := c.Invoke(ctx, nil, map[string]any{"text": "hello", "stream": false})
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}
	got, _ := out["content"].(string)
	if !strings.Contains(got, `<div class="rf-message">hello`) {
		t.Errorf("expected HTML wrap, got %q", got)
	}
}

// TestMessage_OutputFormatInputOverride: the inputs["output_format"]
// key wins over the per-instance declaration.
func TestMessage_OutputFormatInputOverride(t *testing.T) {
	c, _ := NewMessageComponent(map[string]any{
		"text":          "hi",
		"output_format": "html",
	})
	state := canvas.NewCanvasState("r1", "t1")
	ctx := withStateForTest(context.Background(), state)

	out, err := c.Invoke(ctx, nil, map[string]any{
		"text":          "hi",
		"output_format": "plain",
	})
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}
	got, _ := out["content"].(string)
	if got != "hi" {
		t.Errorf("expected plain passthrough, got %q", got)
	}
}

// TestMessage_DownloadsExtraction: a {doc_id, filename, mime_type}
// entry in inputs is surfaced under outputs["downloads"].
func TestMessage_DownloadsExtraction(t *testing.T) {
	c, _ := NewMessageComponent(map[string]any{"text": "see attachment"})
	state := canvas.NewCanvasState("r1", "t1")
	ctx := withStateForTest(context.Background(), state)

	dl := map[string]any{
		"doc_id":    "d-1",
		"filename":  "report.csv",
		"mime_type": "text/csv",
		"url":       "/dl/d-1",
	}
	out, err := c.Invoke(ctx, nil, map[string]any{
		"text":   "see attachment",
		"stream": false,
		"attach": dl,
	})
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}
	dls, ok := out["downloads"].([]DownloadInfo)
	if !ok {
		t.Fatalf("downloads key missing or wrong type: %T", out["downloads"])
	}
	if len(dls) != 1 || dls[0].DocID != "d-1" || dls[0].Filename != "report.csv" {
		t.Errorf("unexpected downloads: %+v", dls)
	}
	// With default plain format, the rendered body does NOT embed
	// the download descriptor (Python's _stringify_message_value
	// returns "" for download-only inputs); we only assert the
	// downloads key is populated.
	if !strings.Contains(out["content"].(string), "see attachment") {
		t.Errorf("body not preserved: %q", out["content"])
	}
}

func TestMessage_DownloadJSONStringSuppressesContent(t *testing.T) {
	c, _ := NewMessageComponent(map[string]any{"text": "unused"})
	state := canvas.NewCanvasState("r1", "t1")
	ctx := withStateForTest(context.Background(), state)
	downloadJSON := `{"doc_id":"d-1","filename":"report.md","mime_type":"text/markdown","url":"/api/v1/agents/attachments/d-1/download","include_download_info_in_content":true}`

	out, err := c.Invoke(ctx, nil, map[string]any{
		"text":   downloadJSON,
		"stream": false,
	})
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}
	dls, ok := out["downloads"].([]DownloadInfo)
	if !ok {
		t.Fatalf("downloads key missing or wrong type: %T", out["downloads"])
	}
	if len(dls) != 1 || dls[0].DocID != "d-1" || dls[0].Filename != "report.md" {
		t.Fatalf("unexpected downloads: %+v", dls)
	}
	if got, _ := out["content"].(string); got != "" {
		t.Fatalf("content = %q, want empty download-only message", got)
	}
}

// TestMessage_AutoPlay_NoEngine: when auto_play is enabled but no
// real synthesizer is registered, the textual content is still
// returned and outputs["audio_error"] surfaces the deferred
// state.
func TestMessage_AutoPlay_NoEngine(t *testing.T) {
	// Force the stub (the default) so the test does not depend on
	// what previous tests installed.
	audio.SetSynthesizer(nil)
	c, _ := NewMessageComponent(map[string]any{
		"text":      "hi",
		"auto_play": true,
	})
	state := canvas.NewCanvasState("r1", "t1")
	ctx := withStateForTest(context.Background(), state)

	out, err := c.Invoke(ctx, nil, map[string]any{"text": "hi", "stream": false})
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}
	if _, ok := out["audio"]; ok {
		t.Errorf("expected no audio key, got %+v", out["audio"])
	}
	if _, ok := out["audio_error"]; !ok {
		t.Errorf("expected audio_error key, got %+v", out)
	}
}

// TestMessage_AutoPlay_Success: with a fake synthesizer that
// returns bytes, outputs["audio"] is populated with the right
// envelope shape.
func TestMessage_AutoPlay_Success(t *testing.T) {
	got := audio.SynthesizeRequest{}
	prev := audio.GetSynthesizer()
	audio.SetSynthesizer(&fakeSynthesizer{
		resp: &audio.SynthesizeResponse{Audio: []byte("abc"), MediaType: "audio/mpeg"},
		got:  &got,
	})
	defer audio.SetSynthesizer(prev)

	c, _ := NewMessageComponent(map[string]any{
		"text":      "hi",
		"auto_play": "gtts",
		"voice":     "en",
		"lang":      "en",
	})
	state := canvas.NewCanvasState("r1", "t1")
	ctx := withStateForTest(context.Background(), state)

	out, err := c.Invoke(ctx, nil, map[string]any{"text": "hi", "stream": false})
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}
	env, ok := out["audio"].(map[string]any)
	if !ok {
		t.Fatalf("expected audio envelope, got %T", out["audio"])
	}
	if env["media_type"] != "audio/mpeg" {
		t.Errorf("media_type: got %v, want audio/mpeg", env["media_type"])
	}
	if got.Engine != audio.EngineGTTS {
		t.Errorf("engine forwarded: got %q, want gtts", got.Engine)
	}
	if got.Voice != "en" {
		t.Errorf("voice forwarded: got %q, want en", got.Voice)
	}
	if got.Lang != "en" {
		t.Errorf("lang forwarded: got %q, want en", got.Lang)
	}
}

// TestMessage_MemorySave_NoService: with memory_save=true and no
// MemorySaver registered, the textual content flows and
// outputs["memory_error"] surfaces the deferred state.
func TestMessage_MemorySave_NoService(t *testing.T) {
	SetMemorySaver(nil)
	c, _ := NewMessageComponent(map[string]any{"text": "hi"})
	state := canvas.NewCanvasState("run-x", "task-x")
	state.Sys["query"] = "what?"
	ctx := withStateForTest(context.Background(), state)

	out, err := c.Invoke(ctx, nil, map[string]any{
		"text":        "hi",
		"stream":      false,
		"memory_save": true,
		"memory_ids":  []string{"m1"},
	})
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}
	if got, _ := out["content"].(string); got != "hi" {
		t.Errorf("content: got %q, want 'hi'", got)
	}
	if _, ok := out["memory_error"]; !ok {
		t.Errorf("expected memory_error key, got %+v", out)
	}
}

// TestMessage_MemorySave_Success: with a custom MemorySaver,
// Save is called with the right fields.
func TestMessage_MemorySave_Success(t *testing.T) {
	var saved MemorySaveRequest
	saveFn := memSaverFunc(func(_ context.Context, req MemorySaveRequest) error {
		saved = req
		return nil
	})
	SetMemorySaver(&saveFn)
	defer SetMemorySaver(nil)

	c, _ := NewMessageComponent(map[string]any{"text": "hi"})
	state := canvas.NewCanvasState("run-y", "session-y")
	state.Sys["query"] = "what?"
	state.Sys["canvas_id"] = "canvas-y"
	state.Sys["session_id"] = "session-y"
	state.Sys["agent_id"] = "agent-y"
	ctx := withStateForTest(context.Background(), state)

	_, err := c.Invoke(ctx, nil, map[string]any{
		"text":        "hi",
		"stream":      false,
		"memory_save": true,
		"memory_ids":  []string{"m1", "m2"},
	})
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}
	if len(saved.MemoryIDs) != 2 || saved.MemoryIDs[0] != "m1" || saved.MemoryIDs[1] != "m2" {
		t.Errorf("MemoryIDs: %+v", saved.MemoryIDs)
	}
	if saved.AgentID != "agent-y" {
		t.Errorf("AgentID: got %q, want agent-y", saved.AgentID)
	}
	if saved.SessionID != "session-y" {
		t.Errorf("SessionID: got %q, want session-y", saved.SessionID)
	}
	if saved.UserInput != "what?" {
		t.Errorf("UserInput: got %q, want what?", saved.UserInput)
	}
	if saved.AgentResponse != "hi" {
		t.Errorf("AgentResponse: got %q, want hi", saved.AgentResponse)
	}
}

func TestMessage_MemorySave_FallbackIDs(t *testing.T) {
	var saved MemorySaveRequest
	saveFn := memSaverFunc(func(_ context.Context, req MemorySaveRequest) error {
		saved = req
		return nil
	})
	SetMemorySaver(&saveFn)
	defer SetMemorySaver(nil)

	c, _ := NewMessageComponent(map[string]any{"text": "hi"})
	state := canvas.NewCanvasState("run-fallback", "task-fallback")
	state.Sys["query"] = "what?"
	ctx := withStateForTest(context.Background(), state)

	_, err := c.Invoke(ctx, nil, map[string]any{
		"text":        "hi",
		"stream":      false,
		"memory_save": true,
		"memory_ids":  []string{"m1"},
	})
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}
	if saved.AgentID != "task-fallback" {
		t.Errorf("AgentID fallback: got %q, want task-fallback", saved.AgentID)
	}
	if saved.SessionID != "run-fallback" {
		t.Errorf("SessionID fallback: got %q, want run-fallback", saved.SessionID)
	}
}

// TestMessage_MemorySave_FromDSLParams: memory_ids declared in the
// DSL params (constructor) are honoured even when the runtime inputs
// map does not carry memory_ids. This is the production path where
// the pipeline only provides upstream component outputs.
func TestMessage_MemorySave_FromDSLParams(t *testing.T) {
	var saved MemorySaveRequest
	saveFn := memSaverFunc(func(_ context.Context, req MemorySaveRequest) error {
		saved = req
		return nil
	})
	SetMemorySaver(&saveFn)
	defer SetMemorySaver(nil)

	c, _ := NewMessageComponent(map[string]any{
		"text":       "hi",
		"memory_ids": []string{"dsl-m1", "dsl-m2"},
	})
	state := canvas.NewCanvasState("run-dsl", "task-dsl")
	state.Sys["query"] = "hello?"
	ctx := withStateForTest(context.Background(), state)

	// Inputs simulate what the pipeline actually provides: only upstream
	// outputs, NO memory_ids or memory_save keys.
	_, err := c.Invoke(ctx, nil, map[string]any{
		"text":   "hi",
		"stream": false,
	})
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}
	if len(saved.MemoryIDs) != 2 || saved.MemoryIDs[0] != "dsl-m1" || saved.MemoryIDs[1] != "dsl-m2" {
		t.Errorf("MemoryIDs from DSL params: %+v", saved.MemoryIDs)
	}
	if saved.AgentResponse != "hi" {
		t.Errorf("AgentResponse: got %q, want hi", saved.AgentResponse)
	}
}

// TestMessage_MemorySave_UserIDVariable: when user_id is a canvas
// variable reference (e.g. "{begin@user_id}"), it is resolved
// against the current state before being passed to the saver.
// Mirrors Python agent/component/message.py:569-571.
func TestMessage_MemorySave_UserIDVariable(t *testing.T) {
	var saved MemorySaveRequest
	saveFn := memSaverFunc(func(_ context.Context, req MemorySaveRequest) error {
		saved = req
		return nil
	})
	SetMemorySaver(&saveFn)
	defer SetMemorySaver(nil)

	c, _ := NewMessageComponent(map[string]any{
		"text":    "hi",
		"user_id": "{begin@user_id}",
	})
	state := canvas.NewCanvasState("run-uid", "task-uid")
	state.Sys["query"] = "hello?"
	state.SetVar("begin", "user_id", "resolved-user-123")
	ctx := withStateForTest(context.Background(), state)

	_, err := c.Invoke(ctx, nil, map[string]any{
		"text":        "hi",
		"stream":      false,
		"memory_save": true,
		"memory_ids":  []string{"m1"},
	})
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}
	if saved.UserID != "resolved-user-123" {
		t.Errorf("UserID: got %q, want resolved-user-123", saved.UserID)
	}
}

// TestMessage_MemorySave_UserIDLiteral: a plain (non-variable)
// user_id is passed through as-is.
func TestMessage_MemorySave_UserIDLiteral(t *testing.T) {
	var saved MemorySaveRequest
	saveFn := memSaverFunc(func(_ context.Context, req MemorySaveRequest) error {
		saved = req
		return nil
	})
	SetMemorySaver(&saveFn)
	defer SetMemorySaver(nil)

	c, _ := NewMessageComponent(map[string]any{
		"text":    "hi",
		"user_id": "plain-user-456",
	})
	state := canvas.NewCanvasState("run-uid2", "task-uid2")
	state.Sys["query"] = "hello?"
	ctx := withStateForTest(context.Background(), state)

	_, err := c.Invoke(ctx, nil, map[string]any{
		"text":        "hi",
		"stream":      false,
		"memory_save": true,
		"memory_ids":  []string{"m1"},
	})
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}
	if saved.UserID != "plain-user-456" {
		t.Errorf("UserID: got %q, want plain-user-456", saved.UserID)
	}
}

// memSaverFunc adapts a closure to the MemorySaver interface so
// tests can record the call inline.
type memSaverFunc func(ctx context.Context, req MemorySaveRequest) error

func (f memSaverFunc) Save(ctx context.Context, req MemorySaveRequest) error {
	return f(ctx, req)
}
