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

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"ragflow/internal/common"
	"ragflow/internal/entity"

	"github.com/gin-gonic/gin"
)

// mockFileCommitSvc implements FileCommitServiceInterface for testing
type mockFileCommitSvc struct {
	createCommitFn          func(ctx context.Context, folderID, authorID, message string, changes []entity.FileChange) (*entity.FileCommit, error)
	listCommitsFn           func(ctx context.Context, folderID string, page, pageSize int, orderBy string, desc bool) ([]*entity.FileCommit, int64, error)
	getCommitFn             func(ctx context.Context, commitID string) (*entity.FileCommit, error)
	listCommitFilesFn       func(ctx context.Context, commitID string) ([]*entity.FileCommitItem, error)
	diffCommitsFn           func(ctx context.Context, fromID, toID string) ([]entity.DiffEntry, error)
	getUncommittedChangesFn func(ctx context.Context, folderID string) ([]entity.DiffEntry, error)
	getCommitTreeFn         func(ctx context.Context, commitID string) (map[string]interface{}, error)
	getCommitFileContentFn  func(ctx context.Context, folderID, commitID, fileID string) ([]byte, error)
	getFileVersionHistoryFn func(ctx context.Context, fileID string) ([]entity.VersionEntry, error)
	listPageCommitsFn       func(ctx context.Context, datasetID, pageType, slug string, page, pageSize int) ([]*entity.FileCommit, int64, error)
}

func (m *mockFileCommitSvc) CreateCommit(ctx context.Context, folderID, authorID, message string, changes []entity.FileChange) (*entity.FileCommit, error) {
	if m.createCommitFn != nil {
		return m.createCommitFn(ctx, folderID, authorID, message, changes)
	}
	return &entity.FileCommit{
		ID:        "commit-1",
		FolderID:  folderID,
		Message:   message,
		AuthorID:  authorID,
		FileCount: len(changes),
	}, nil
}

func (m *mockFileCommitSvc) ListCommits(ctx context.Context, folderID string, page, pageSize int, orderBy string, desc bool) ([]*entity.FileCommit, int64, error) {
	if m.listCommitsFn != nil {
		return m.listCommitsFn(ctx, folderID, page, pageSize, orderBy, desc)
	}
	now := int64(1718200000000)
	return []*entity.FileCommit{
		{ID: "c2", FolderID: folderID, Message: "second", AuthorID: "u1", FileCount: 1, BaseModel: entity.BaseModel{CreateTime: &now}},
		{ID: "c1", FolderID: folderID, Message: "first", AuthorID: "u1", FileCount: 2, BaseModel: entity.BaseModel{CreateTime: &now}},
	}, 2, nil
}

func (m *mockFileCommitSvc) GetCommit(ctx context.Context, commitID string) (*entity.FileCommit, error) {
	if m.getCommitFn != nil {
		return m.getCommitFn(ctx, commitID)
	}
	return &entity.FileCommit{ID: commitID, FolderID: "folder-1", Message: "test commit", AuthorID: "u1", FileCount: 1}, nil
}

func (m *mockFileCommitSvc) ListCommitFiles(ctx context.Context, commitID string) ([]*entity.FileCommitItem, error) {
	if m.listCommitFilesFn != nil {
		return m.listCommitFilesFn(ctx, commitID)
	}
	return []*entity.FileCommitItem{
		{ID: "i1", CommitID: commitID, FileID: "f1", Operation: "add"},
	}, nil
}

func (m *mockFileCommitSvc) DiffCommits(ctx context.Context, fromID, toID string) ([]entity.DiffEntry, error) {
	if m.diffCommitsFn != nil {
		return m.diffCommitsFn(ctx, fromID, toID)
	}
	return []entity.DiffEntry{
		{FileID: "f1", FileName: "file.txt", Operation: "modify"},
	}, nil
}

func (m *mockFileCommitSvc) GetUncommittedChanges(ctx context.Context, folderID string) ([]entity.DiffEntry, error) {
	if m.getUncommittedChangesFn != nil {
		return m.getUncommittedChangesFn(ctx, folderID)
	}
	return []entity.DiffEntry{
		{FileID: "f1", FileName: "new.txt", Operation: "add"},
	}, nil
}

func (m *mockFileCommitSvc) GetCommitTree(ctx context.Context, commitID string) (map[string]interface{}, error) {
	if m.getCommitTreeFn != nil {
		return m.getCommitTreeFn(ctx, commitID)
	}
	return map[string]interface{}{
		"f1": map[string]interface{}{"name": "file.txt", "hash": "abc123", "size": 100, "status": "1"},
	}, nil
}

func (m *mockFileCommitSvc) GetCommitFileContent(ctx context.Context, folderID, commitID, fileID string) ([]byte, error) {
	if m.getCommitFileContentFn != nil {
		return m.getCommitFileContentFn(ctx, folderID, commitID, fileID)
	}
	return []byte("file content"), nil
}

func (m *mockFileCommitSvc) GetFileVersionHistory(ctx context.Context, fileID string) ([]entity.VersionEntry, error) {
	if m.getFileVersionHistoryFn != nil {
		return m.getFileVersionHistoryFn(ctx, fileID)
	}
	now := int64(1718200000000)
	return []entity.VersionEntry{
		{CommitID: "c2", Operation: "modify", Hash: "def456", CreateTime: &now, Message: "updated"},
		{CommitID: "c1", Operation: "add", Hash: "abc123", CreateTime: &now, Message: "initial"},
	}, nil
}

func (m *mockFileCommitSvc) ListPageCommits(ctx context.Context, datasetID, pageType, slug string, page, pageSize int) ([]*entity.FileCommit, int64, error) {
	if m.listPageCommitsFn != nil {
		return m.listPageCommitsFn(ctx, datasetID, pageType, slug, page, pageSize)
	}
	return []*entity.FileCommit{}, 0, nil
}

func setupFileCommitTest(userID string) (*gin.Engine, *mockFileCommitSvc) {
	mock := &mockFileCommitSvc{}
	h := &FileCommitHandler{commitService: mock}
	gin.SetMode(gin.TestMode)
	r := gin.New()
	r.Use(func(c *gin.Context) {
		c.Set("user", &entity.User{ID: userID})
	})
	r.POST("/api/v1/folders/:folder_id/commits", h.CreateCommit)
	r.GET("/api/v1/folders/:folder_id/commits", h.ListCommits)
	r.GET("/api/v1/folders/:folder_id/commits/:commit_id", h.GetCommit)
	r.GET("/api/v1/folders/:folder_id/commits/:commit_id/files", h.ListCommitFiles)
	r.GET("/api/v1/folders/:folder_id/commits/diff", h.DiffCommits)
	r.GET("/api/v1/folders/:folder_id/changes", h.GetUncommittedChanges)
	r.GET("/api/v1/folders/:folder_id/commits/:commit_id/tree", h.GetCommitTree)
	r.GET("/api/v1/folders/:folder_id/commits/:commit_id/files/:file_id/content", h.GetCommitFileContent)
	r.GET("/api/v1/files/:id/versions", h.GetFileVersionHistory)
	return r, mock
}

func setupFileCommitTestNoAuth() *gin.Engine {
	h := &FileCommitHandler{}
	gin.SetMode(gin.TestMode)
	r := gin.New()
	r.POST("/api/v1/folders/:folder_id/commits", h.CreateCommit)
	return r
}

// ── Tests ────────────────────────────────────────────────────────────────

func TestFileCommit_CreateCommit_Success(t *testing.T) {
	r, mock := setupFileCommitTest("user-1")
	mock.createCommitFn = func(ctx context.Context, folderID, authorID, message string, changes []entity.FileChange) (*entity.FileCommit, error) {
		if folderID != "folder-1" {
			t.Errorf("expected folder-1, got %s", folderID)
		}
		if authorID != "user-1" {
			t.Errorf("expected user-1, got %s", authorID)
		}
		if message != "initial commit" {
			t.Errorf("expected 'initial commit', got %s", message)
		}
		if len(changes) != 1 || changes[0].FileID != "f1" {
			t.Errorf("unexpected changes: %+v", changes)
		}
		now := int64(1718200000000)
		return &entity.FileCommit{
			ID: "commit-1", FolderID: folderID, Message: message,
			AuthorID: authorID, FileCount: len(changes),
			BaseModel: entity.BaseModel{CreateTime: &now},
		}, nil
	}

	body := `{"message": "initial commit", "files": [{"file_id": "f1", "file_name": "test.txt", "operation": "add", "content": "hello"}]}`
	w := httptest.NewRecorder()
	req, _ := http.NewRequest("POST", "/api/v1/folders/folder-1/commits", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	r.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
	}
	var resp map[string]interface{}
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatal(err)
	}
	if resp["code"] != float64(common.CodeSuccess) {
		t.Errorf("expected code 0, got %v", resp["code"])
	}
	data, ok := resp["data"].(map[string]interface{})
	if !ok {
		t.Fatalf("expected data to be object, got %T", resp["data"])
	}
	if data["message"] != "initial commit" {
		t.Errorf("expected 'initial commit', got %v", data["message"])
	}
}

func TestFileCommit_CreateCommit_NoAuth(t *testing.T) {
	r := setupFileCommitTestNoAuth()
	body := `{"message": "test", "files": [{"file_id": "f1", "file_name": "t.txt", "operation": "add"}]}`
	w := httptest.NewRecorder()
	req, _ := http.NewRequest("POST", "/api/v1/folders/folder-1/commits", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	r.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", w.Code)
	}
	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	// No auth middleware → code 401
	if code, _ := resp["code"].(float64); code != float64(common.CodeUnauthorized) {
		t.Errorf("expected unauthorized, got code %v", code)
	}
}

func TestFileCommit_CreateCommit_InvalidJSON(t *testing.T) {
	r, _ := setupFileCommitTest("user-1")
	body := `{invalid json`
	w := httptest.NewRecorder()
	req, _ := http.NewRequest("POST", "/api/v1/folders/folder-1/commits", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	r.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", w.Code)
	}
	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	if code, _ := resp["code"].(float64); code != float64(common.CodeBadRequest) {
		t.Errorf("expected bad request, got code %v", code)
	}
}

func TestFileCommit_ListCommits_Success(t *testing.T) {
	r, mock := setupFileCommitTest("user-1")
	mock.listCommitsFn = func(ctx context.Context, folderID string, page, pageSize int, orderBy string, desc bool) ([]*entity.FileCommit, int64, error) {
		if folderID != "folder-1" {
			t.Errorf("expected folder-1, got %s", folderID)
		}
		return []*entity.FileCommit{
			{ID: "c2", FolderID: folderID, Message: "second", AuthorID: "u1", FileCount: 1},
			{ID: "c1", FolderID: folderID, Message: "first", AuthorID: "u1", FileCount: 2},
		}, 2, nil
	}

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/api/v1/folders/folder-1/commits?page=1&page_size=10", nil)
	r.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
	}
	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	if resp["code"] != float64(common.CodeSuccess) {
		t.Errorf("expected code 0, got %v", resp["code"])
	}
	data, _ := resp["data"].(map[string]interface{})
	if total, _ := data["total"].(float64); total != 2 {
		t.Errorf("expected total 2, got %v", total)
	}
}

func TestFileCommit_GetCommit_Success(t *testing.T) {
	r, _ := setupFileCommitTest("user-1")

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/api/v1/folders/folder-1/commits/commit-1", nil)
	r.ServeHTTP(w, req)

	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	if resp["code"] != float64(common.CodeSuccess) {
		t.Errorf("expected success, got code %v: %s", resp["code"], resp["message"])
	}
	data, _ := resp["data"].(map[string]interface{})
	if data["id"] != "commit-1" {
		t.Errorf("expected commit-1, got %v", data["id"])
	}
}

func TestFileCommit_GetCommit_NotFound(t *testing.T) {
	r, mock := setupFileCommitTest("user-1")
	mock.getCommitFn = func(ctx context.Context, commitID string) (*entity.FileCommit, error) {
		return nil, common.ErrNotFound
	}

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/api/v1/folders/folder-1/commits/missing", nil)
	r.ServeHTTP(w, req)

	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	if code, _ := resp["code"].(float64); code != float64(common.CodeNotFound) {
		t.Errorf("expected 404, got code %v", code)
	}
}

func TestFileCommit_ListCommitFiles_Success(t *testing.T) {
	r, _ := setupFileCommitTest("user-1")

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/api/v1/folders/folder-1/commits/commit-1/files", nil)
	r.ServeHTTP(w, req)

	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	if resp["code"] != float64(common.CodeSuccess) {
		t.Errorf("expected success, got code %v", resp["code"])
	}
}

func TestFileCommit_DiffCommits_Success(t *testing.T) {
	r, _ := setupFileCommitTest("user-1")

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/api/v1/folders/folder-1/commits/diff?from=c1&to=c2", nil)
	r.ServeHTTP(w, req)

	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	if resp["code"] != float64(common.CodeSuccess) {
		t.Errorf("expected success, got code %v", resp["code"])
	}
}

func TestFileCommit_DiffCommits_MissingParams(t *testing.T) {
	r, _ := setupFileCommitTest("user-1")

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/api/v1/folders/folder-1/commits/diff", nil)
	r.ServeHTTP(w, req)

	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	if code, _ := resp["code"].(float64); code != float64(common.CodeParamError) {
		t.Errorf("expected param error, got code %v", code)
	}
}

func TestFileCommit_GetUncommittedChanges_Success(t *testing.T) {
	r, _ := setupFileCommitTest("user-1")

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/api/v1/folders/folder-1/changes", nil)
	r.ServeHTTP(w, req)

	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	if resp["code"] != float64(common.CodeSuccess) {
		t.Errorf("expected success, got code %v", resp["code"])
	}
}

func TestFileCommit_GetCommitTree_Success(t *testing.T) {
	r, _ := setupFileCommitTest("user-1")

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/api/v1/folders/folder-1/commits/commit-1/tree", nil)
	r.ServeHTTP(w, req)

	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	if resp["code"] != float64(common.CodeSuccess) {
		t.Errorf("expected success, got code %v", resp["code"])
	}
}

func TestFileCommit_GetCommitFileContent_Success(t *testing.T) {
	r, mock := setupFileCommitTest("user-1")
	mock.getCommitFileContentFn = func(ctx context.Context, folderID, commitID, fileID string) ([]byte, error) {
		return []byte("hello world"), nil
	}

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/api/v1/folders/folder-1/commits/commit-1/files/f1/content", nil)
	r.ServeHTTP(w, req)

	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	if resp["code"] != float64(common.CodeSuccess) {
		t.Errorf("expected success, got code %v", resp["code"])
	}
	data, _ := resp["data"].(map[string]interface{})
	if content, _ := data["content"].(string); content != "hello world" {
		t.Errorf("expected 'hello world', got %q", content)
	}
}

func TestFileCommit_GetFileVersionHistory_Success(t *testing.T) {
	r, _ := setupFileCommitTest("user-1")

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/api/v1/files/f1/versions", nil)
	r.ServeHTTP(w, req)

	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	if resp["code"] != float64(common.CodeSuccess) {
		t.Errorf("expected success, got code %v", resp["code"])
	}
}
