//
//  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 (
	"strings"
	"testing"

	"ragflow/internal/agent/runtime"
	"ragflow/internal/dao"
	"ragflow/internal/entity"
	"ragflow/internal/storage"

	"github.com/glebarez/sqlite"
	"gorm.io/gorm"
)

// withMemoryStorage swaps the global storage factory for an
// in-memory implementation and restores the previous backend on
// cleanup. Mirrors the pattern in internal/service/file_test.go:80-83.
func withMemoryStorage(t *testing.T) *storage.MemoryStorage {
	t.Helper()
	factory := storage.GetStorageFactory()
	prev := factory.GetStorage()
	ms := storage.NewMemoryStorage().(*storage.MemoryStorage)
	factory.SetStorage(ms)
	t.Cleanup(func() { factory.SetStorage(prev) })
	return ms
}

func withFileComponentTestDB(t *testing.T) *gorm.DB {
	t.Helper()
	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{TranslateError: true})
	if err != nil {
		t.Fatalf("failed to open sqlite: %v", err)
	}
	if err := db.AutoMigrate(&entity.Document{}, &entity.File{}, &entity.File2Document{}); err != nil {
		t.Fatalf("failed to migrate sqlite: %v", err)
	}
	prev := dao.DB
	dao.DB = db
	t.Cleanup(func() { dao.DB = prev })
	return db
}

// TestFileComponent_Registered verifies the init() registration
// is visible to the runtime registry (Phase 4 / API layer
// depends on this).
func TestFileComponent_Registered(t *testing.T) {
	factory, cat, md, ok := runtime.DefaultRegistry.Lookup("File")
	if !ok {
		t.Fatal("File not registered in runtime.DefaultRegistry")
	}
	if cat != runtime.CategoryIngestion {
		t.Errorf("category = %q, want %q", cat, runtime.CategoryIngestion)
	}
	if factory == nil {
		t.Error("factory is nil")
	}
	if md.Inputs == nil || len(md.Inputs) == 0 {
		t.Errorf("metadata.Inputs empty: %v", md.Inputs)
	}
	if md.Outputs == nil || len(md.Outputs) == 0 {
		t.Errorf("metadata.Outputs empty: %v", md.Outputs)
	}
}

// TestFileComponent_Invoke_HappyPath verifies File remains metadata-only
// even when explicit storage overrides are supplied.
func TestFileComponent_Invoke_HappyPath(t *testing.T) {
	ms := withMemoryStorage(t)
	if err := ms.Put("bucketA", "path/to/file.txt", []byte("hello, ragflow")); err != nil {
		t.Fatalf("seed: %v", err)
	}

	c := &FileComponent{}
	out, err := c.Invoke(t.Context(), nil, map[string]any{
		"file":   []map[string]any{{"name": "file.txt"}},
		"bucket": "bucketA",
		"path":   "path/to/file.txt",
	})
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}

	if got := out["name"]; got != "file.txt" {
		t.Errorf("name = %v, want file.txt", got)
	}
	if out["bucket"] != "bucketA" {
		t.Errorf("bucket = %v, want bucketA", out["bucket"])
	}
	if out["path"] != "path/to/file.txt" {
		t.Errorf("path = %v, want path/to/file.txt", out["path"])
	}
	if _, ok := out["binary"]; ok {
		t.Fatalf("binary should not be emitted by File: %v", out["binary"])
	}
}

func TestFileComponent_Invoke_ResolvesDocIDViaDocumentLocation(t *testing.T) {
	ms := withMemoryStorage(t)
	db := withFileComponentTestDB(t)
	location := "docs/from-document.bin"
	if err := ms.Put("kb-doc", location, []byte("doc-location")); err != nil {
		t.Fatalf("seed storage: %v", err)
	}
	docName := "report.pdf"
	if err := db.Create(&entity.Document{
		ID:           "doc-loc",
		KbID:         "kb-doc",
		ParserID:     "na",
		ParserConfig: entity.JSONMap{},
		Type:         "pdf",
		CreatedBy:    "u1",
		Name:         &docName,
		Location:     &location,
		Suffix:       ".pdf",
	}).Error; err != nil {
		t.Fatalf("seed doc: %v", err)
	}

	c := &FileComponent{}
	out, err := c.Invoke(t.Context(), db, map[string]any{"doc_id": "doc-loc"})
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}
	if out["name"] != "report.pdf" {
		t.Fatalf("name = %v, want report.pdf", out["name"])
	}
	if _, ok := out["bucket"]; ok {
		t.Fatalf("bucket should not be emitted for doc_id-only path: %v", out["bucket"])
	}
	if _, ok := out["path"]; ok {
		t.Fatalf("path should not be emitted for doc_id-only path: %v", out["path"])
	}
}

func TestFileComponent_Invoke_ResolvesDocIDViaFileMapping(t *testing.T) {
	ms := withMemoryStorage(t)
	db := withFileComponentTestDB(t)
	location := "tenant-root/from-file.bin"
	if err := ms.Put("folder-1", location, []byte("file-mapping")); err != nil {
		t.Fatalf("seed storage: %v", err)
	}
	docName := "deck.pptx"
	if err := db.Create(&entity.Document{
		ID:           "doc-file",
		KbID:         "kb-1",
		ParserID:     "na",
		ParserConfig: entity.JSONMap{},
		Type:         "ppt",
		CreatedBy:    "u1",
		Name:         &docName,
		Suffix:       ".pptx",
	}).Error; err != nil {
		t.Fatalf("seed doc: %v", err)
	}
	if err := db.Create(&entity.File{
		ID:        "file-1",
		ParentID:  "folder-1",
		TenantID:  "tenant-1",
		CreatedBy: "u1",
		Name:      "deck.pptx",
		Location:  &location,
		Type:      "file",
	}).Error; err != nil {
		t.Fatalf("seed file: %v", err)
	}
	fileID := "file-1"
	docID := "doc-file"
	if err := db.Create(&entity.File2Document{
		ID:         "map-1",
		FileID:     &fileID,
		DocumentID: &docID,
	}).Error; err != nil {
		t.Fatalf("seed mapping: %v", err)
	}

	c := &FileComponent{}
	out, err := c.Invoke(t.Context(), db, map[string]any{"doc_id": "doc-file"})
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}
	if out["name"] != "deck.pptx" {
		t.Fatalf("name = %v, want deck.pptx", out["name"])
	}
	if _, ok := out["bucket"]; ok {
		t.Fatalf("bucket should not be emitted for doc_id-only path: %v", out["bucket"])
	}
	if _, ok := out["path"]; ok {
		t.Fatalf("path should not be emitted for doc_id-only path: %v", out["path"])
	}
}

func TestFileComponent_Invoke_DocIDWithoutStorageLocationStillSucceeds(t *testing.T) {
	withMemoryStorage(t)
	db := withFileComponentTestDB(t)
	if err := db.Create(&entity.Document{
		ID:           "doc-empty",
		KbID:         "kb-1",
		ParserID:     "na",
		ParserConfig: entity.JSONMap{},
		Type:         "txt",
		CreatedBy:    "u1",
		Suffix:       ".txt",
	}).Error; err != nil {
		t.Fatalf("seed doc: %v", err)
	}

	c := &FileComponent{}
	out, err := c.Invoke(t.Context(), db, map[string]any{"doc_id": "doc-empty"})
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}
	if got := out["name"]; got != "doc-empty" {
		t.Fatalf("name = %v, want doc-empty", got)
	}
}

// TestFileComponent_Invoke_MissingDoc covers the input-validation
// branch when neither doc_id nor file is supplied.
func TestFileComponent_Invoke_MissingDoc(t *testing.T) {
	withMemoryStorage(t)
	c := &FileComponent{}
	_, err := c.Invoke(t.Context(), nil, map[string]any{})
	if err == nil {
		t.Fatal("expected error for empty inputs, got nil")
	}
	if !strings.Contains(err.Error(), "doc_id") {
		t.Errorf("error should mention doc_id/file: %v", err)
	}
}

// TestFileComponent_Invoke_EchoesStorageOverride verifies explicit
// bucket/path overrides still flow through for downstream Parser use.
func TestFileComponent_Invoke_IncludesCheckpointPath(t *testing.T) {
	ms := withMemoryStorage(t)
	const wantPath = "checkpoint/expected/path.bin"
	if err := ms.Put("b", wantPath, []byte("x")); err != nil {
		t.Fatalf("seed: %v", err)
	}
	c := &FileComponent{}
	out, err := c.Invoke(t.Context(), nil, map[string]any{
		"file":   []map[string]any{{"name": "checkpoint.bin"}},
		"bucket": "b",
		"path":   wantPath,
	})
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}
	if out["path"] != wantPath {
		t.Errorf("path = %v, want %q", out["path"], wantPath)
	}
	if out["bucket"] != "b" {
		t.Errorf("bucket = %v, want b", out["bucket"])
	}
	if _, ok := out["binary"]; ok {
		t.Fatalf("binary should not be emitted by File: %v", out["binary"])
	}
}

// TestFileComponent_InputsOutputs_NonEmpty is the shape
// assertion Phase 4's API endpoint relies on.
func TestFileComponent_InputsOutputs_NonEmpty(t *testing.T) {
	c := &FileComponent{}
	ins := c.Inputs()
	outs := c.Outputs()
	if len(ins) == 0 {
		t.Error("Inputs() returned empty map")
	}
	if len(outs) == 0 {
		t.Error("Outputs() returned empty map")
	}
	for _, key := range []string{"name", "file"} {
		if _, ok := outs[key]; !ok {
			t.Errorf("Outputs() missing %q", key)
		}
	}
}
