//
//  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.
//

// memory_message_service_test.go — MemorySaver port tests.
//
// Coverage focuses on the synchronous parts (memory lookup + raw
// message construction + result aggregation).

package service

import (
	"context"
	"strings"
	"testing"
)

// TestQueueSaveToMemoryTask_NilService: a nil receiver surfaces
// a clear error rather than panicking.
func TestQueueSaveToMemoryTask_NilService(t *testing.T) {
	var s *MemoryMessageService
	_, err := s.QueueSaveToMemoryTask(context.Background(), []string{"m1"}, MemoryMessage{AgentID: "a1"})
	if err == nil {
		t.Fatal("expected error from nil service")
	}
	if !strings.Contains(err.Error(), "nil") {
		t.Errorf("error = %v, want nil-service error", err)
	}
}

// TestQueueSaveToMemoryTask_EmptyMemoryList: an empty input
// short-circuits to an empty result with no error.
func TestQueueSaveToMemoryTask_EmptyMemoryList(t *testing.T) {
	s := &MemoryMessageService{memories: nil} // no lookups happen
	res, err := s.QueueSaveToMemoryTask(context.Background(), nil, MemoryMessage{AgentID: "a1"})
	if err != nil {
		t.Fatalf("QueueSaveToMemoryTask: %v", err)
	}
	if len(res.NotFound) != 0 || len(res.Failed) != 0 {
		t.Errorf("expected empty result, got %+v", res)
	}
}

// TestQueueSaveToMemoryTask_MissingAgentID: AgentID is required
// for the row envelope; an empty AgentID surfaces a clear error
// up front.
func TestQueueSaveToMemoryTask_MissingAgentID(t *testing.T) {
	s := &MemoryMessageService{}
	_, err := s.QueueSaveToMemoryTask(context.Background(), []string{"m1"}, MemoryMessage{})
	if err == nil {
		t.Fatal("expected error for missing AgentID")
	}
	if !strings.Contains(err.Error(), "AgentID") {
		t.Errorf("error = %v, want AgentID-required error", err)
	}
}

// TestBuildRawMessage_EnvelopeShape: the row envelope carries
// every logical field the Python extractor reads. Storage-level
// fields (message_type_kwd, content_ltks, tokenized_content_ltks,
// status_int) are the doc engine's concern and must not be set here.
func TestBuildRawMessage_EnvelopeShape(t *testing.T) {
	raw := buildRawMessage(42, "mem-1", MemoryMessage{
		UserID:        "u1",
		AgentID:       "a1",
		SessionID:     "s1",
		UserInput:     "hi",
		AgentResponse: "hello",
	})

	want := map[string]any{
		"message_id":   int64(42),
		"message_type": "raw",
		"memory_id":    "mem-1",
		"user_id":      "u1",
		"agent_id":     "a1",
		"session_id":   "s1",
		"status":       true,
	}
	for k, want := range want {
		if got := raw[k]; got != want {
			t.Errorf("raw[%q] = %v (%T), want %v (%T)", k, got, got, want, want)
		}
	}
	for _, storageField := range []string{"message_type_kwd", "content_ltks", "tokenized_content_ltks", "status_int"} {
		if _, ok := raw[storageField]; ok {
			t.Errorf("raw[%q] is a storage field and must not be set by the service layer", storageField)
		}
	}

	content, _ := raw["content"].(string)
	if !strings.Contains(content, "User Input: hi") {
		t.Errorf("content missing user input: %q", content)
	}
	if !strings.Contains(content, "Agent Response: hello") {
		t.Errorf("content missing agent response: %q", content)
	}
	if !strings.Contains(content, "\n") {
		t.Errorf("content should have user/agent on separate lines: %q", content)
	}
}

// TestBuildTaskRow_Shape: the task row mirrors the Python Task
// entity's memory-task shape.
func TestBuildTaskRow_Shape(t *testing.T) {
	row := buildTaskRow(99, "mem-1")
	if row["task_type"] != "memory" {
		t.Errorf("task_type = %v, want \"memory\"", row["task_type"])
	}
	if row["doc_id"] != "mem-1" {
		t.Errorf("doc_id = %v, want \"mem-1\"", row["doc_id"])
	}
	if row["progress"] != 0.0 {
		t.Errorf("progress = %v, want 0.0", row["progress"])
	}
	if row["progress_msg"] != "" {
		t.Errorf("progress_msg = %v, want empty string", row["progress_msg"])
	}
	if row["digest"] != "99" {
		t.Errorf("digest = %v, want \"99\"", row["digest"])
	}
	if id, _ := row["id"].(string); len(id) != 32 {
		t.Errorf("id = %q, want 32-char uuid", id)
	}
}

func TestTaskFromRow_InitializesProgressMessage(t *testing.T) {
	task := taskFromRow(buildTaskRow(99, "mem-1"))
	if task.ProgressMsg == nil {
		t.Fatal("ProgressMsg is nil")
	}
	if *task.ProgressMsg != "" {
		t.Fatalf("ProgressMsg = %q, want empty string", *task.ProgressMsg)
	}
}

// TestGenerateRawMessageID_Unique: two calls produce different
// values. (Wall-clock based today; the Redis-backed counter will
// be added when the project's Redis client lands.)
func TestGenerateRawMessageID_Unique(t *testing.T) {
	a := generateRawMessageID()
	b := generateRawMessageID()
	if a == b {
		// Allow a 1-second tie when the clock hasn't ticked.
		// GenerateRawMessageID uses Unix seconds; two calls
		// within the same second will collide. The Redis
		// counter is the eventual fix; this test only
		// verifies monotonic-ish behaviour.
		t.Logf("note: raw_message_ids collided (%d) — within the same second; the Redis counter will fix this", a)
	}
	if a <= 0 || b <= 0 {
		t.Errorf("expected positive ids, got a=%d b=%d", a, b)
	}
}
