package sqlite

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"

	"github.com/DeBrosOfficial/network/pkg/deployments"
	"github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys"
	"github.com/DeBrosOfficial/network/pkg/ipfs"
	"github.com/DeBrosOfficial/network/pkg/rqlite"
	"go.uber.org/zap"
)

// Mock implementations

type mockRQLiteClient struct {
	QueryFunc    func(ctx context.Context, dest interface{}, query string, args ...interface{}) error
	ExecFunc     func(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
	FindByFunc   func(ctx context.Context, dest interface{}, table string, criteria map[string]interface{}, opts ...rqlite.FindOption) error
	FindOneFunc  func(ctx context.Context, dest interface{}, table string, criteria map[string]interface{}, opts ...rqlite.FindOption) error
	SaveFunc     func(ctx context.Context, entity interface{}) error
	RemoveFunc   func(ctx context.Context, entity interface{}) error
	RepoFunc     func(table string) interface{}
	CreateQBFunc func(table string) *rqlite.QueryBuilder
	TxFunc       func(ctx context.Context, fn func(tx rqlite.Tx) error) error
}

func (m *mockRQLiteClient) Query(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
	if m.QueryFunc != nil {
		return m.QueryFunc(ctx, dest, query, args...)
	}
	return nil
}

func (m *mockRQLiteClient) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
	if m.ExecFunc != nil {
		return m.ExecFunc(ctx, query, args...)
	}
	return nil, nil
}

func (m *mockRQLiteClient) FindBy(ctx context.Context, dest interface{}, table string, criteria map[string]interface{}, opts ...rqlite.FindOption) error {
	if m.FindByFunc != nil {
		return m.FindByFunc(ctx, dest, table, criteria, opts...)
	}
	return nil
}

func (m *mockRQLiteClient) FindOneBy(ctx context.Context, dest interface{}, table string, criteria map[string]interface{}, opts ...rqlite.FindOption) error {
	if m.FindOneFunc != nil {
		return m.FindOneFunc(ctx, dest, table, criteria, opts...)
	}
	return nil
}

func (m *mockRQLiteClient) Save(ctx context.Context, entity interface{}) error {
	if m.SaveFunc != nil {
		return m.SaveFunc(ctx, entity)
	}
	return nil
}

func (m *mockRQLiteClient) Remove(ctx context.Context, entity interface{}) error {
	if m.RemoveFunc != nil {
		return m.RemoveFunc(ctx, entity)
	}
	return nil
}

func (m *mockRQLiteClient) Repository(table string) interface{} {
	if m.RepoFunc != nil {
		return m.RepoFunc(table)
	}
	return nil
}

func (m *mockRQLiteClient) CreateQueryBuilder(table string) *rqlite.QueryBuilder {
	if m.CreateQBFunc != nil {
		return m.CreateQBFunc(table)
	}
	return nil
}

func (m *mockRQLiteClient) Tx(ctx context.Context, fn func(tx rqlite.Tx) error) error {
	if m.TxFunc != nil {
		return m.TxFunc(ctx, fn)
	}
	return nil
}

type mockIPFSClient struct {
	AddFunc          func(ctx context.Context, r io.Reader, filename string) (*ipfs.AddResponse, error)
	AddDirectoryFunc func(ctx context.Context, dirPath string) (*ipfs.AddResponse, error)
	GetFunc          func(ctx context.Context, path, ipfsAPIURL string) (io.ReadCloser, error)
	PinFunc          func(ctx context.Context, cid, name string, replicationFactor int) (*ipfs.PinResponse, error)
	PinStatusFunc    func(ctx context.Context, cid string) (*ipfs.PinStatus, error)
	UnpinFunc        func(ctx context.Context, cid string) error
	HealthFunc       func(ctx context.Context) error
	GetPeerFunc      func(ctx context.Context) (int, error)
	CloseFunc        func(ctx context.Context) error
}

func (m *mockIPFSClient) Add(ctx context.Context, r io.Reader, filename string) (*ipfs.AddResponse, error) {
	if m.AddFunc != nil {
		return m.AddFunc(ctx, r, filename)
	}
	return &ipfs.AddResponse{Cid: "QmTestCID123456789"}, nil
}

func (m *mockIPFSClient) AddDirectory(ctx context.Context, dirPath string) (*ipfs.AddResponse, error) {
	if m.AddDirectoryFunc != nil {
		return m.AddDirectoryFunc(ctx, dirPath)
	}
	return &ipfs.AddResponse{Cid: "QmTestDirCID123456789"}, nil
}

func (m *mockIPFSClient) Get(ctx context.Context, cid, ipfsAPIURL string) (io.ReadCloser, error) {
	if m.GetFunc != nil {
		return m.GetFunc(ctx, cid, ipfsAPIURL)
	}
	return io.NopCloser(nil), nil
}

func (m *mockIPFSClient) Pin(ctx context.Context, cid, name string, replicationFactor int) (*ipfs.PinResponse, error) {
	if m.PinFunc != nil {
		return m.PinFunc(ctx, cid, name, replicationFactor)
	}
	return &ipfs.PinResponse{}, nil
}

func (m *mockIPFSClient) PinStatus(ctx context.Context, cid string) (*ipfs.PinStatus, error) {
	if m.PinStatusFunc != nil {
		return m.PinStatusFunc(ctx, cid)
	}
	return &ipfs.PinStatus{}, nil
}

func (m *mockIPFSClient) Unpin(ctx context.Context, cid string) error {
	if m.UnpinFunc != nil {
		return m.UnpinFunc(ctx, cid)
	}
	return nil
}

func (m *mockIPFSClient) Health(ctx context.Context) error {
	if m.HealthFunc != nil {
		return m.HealthFunc(ctx)
	}
	return nil
}

func (m *mockIPFSClient) GetPeerCount(ctx context.Context) (int, error) {
	if m.GetPeerFunc != nil {
		return m.GetPeerFunc(ctx)
	}
	return 5, nil
}

func (m *mockIPFSClient) Close(ctx context.Context) error {
	if m.CloseFunc != nil {
		return m.CloseFunc(ctx)
	}
	return nil
}

// TestCreateDatabase_Success tests creating a new database
func TestCreateDatabase_Success(t *testing.T) {
	mockDB := &mockRQLiteClient{
		QueryFunc: func(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
			// For dns_nodes query, return mock active node
			if strings.Contains(query, "dns_nodes") {
				destValue := reflect.ValueOf(dest)
				if destValue.Kind() == reflect.Ptr {
					sliceValue := destValue.Elem()
					if sliceValue.Kind() == reflect.Slice {
						elemType := sliceValue.Type().Elem()
						newElem := reflect.New(elemType).Elem()
						idField := newElem.FieldByName("ID")
						if idField.IsValid() && idField.CanSet() {
							idField.SetString("node-test123")
						}
						sliceValue.Set(reflect.Append(sliceValue, newElem))
					}
				}
			}
			// For database check, return empty (database doesn't exist)
			if strings.Contains(query, "namespace_sqlite_databases") && strings.Contains(query, "SELECT") {
				// Return empty result
			}
			return nil
		},
		ExecFunc: func(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
			return nil, nil
		},
	}

	// Create temp directory for test database
	tmpDir := t.TempDir()

	portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop())
	homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop())

	handler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop(), "", "")
	handler.basePath = tmpDir

	reqBody := map[string]string{
		"database_name": "test-db",
	}
	bodyBytes, _ := json.Marshal(reqBody)

	req := httptest.NewRequest("POST", "/v1/db/sqlite/create", bytes.NewReader(bodyBytes))
	ctx := context.WithValue(req.Context(), ctxkeys.NamespaceOverride, "test-namespace")
	req = req.WithContext(ctx)

	rr := httptest.NewRecorder()

	handler.CreateDatabase(rr, req)

	if rr.Code != http.StatusCreated {
		t.Errorf("Expected status 201, got %d", rr.Code)
		t.Logf("Response: %s", rr.Body.String())
	}

	// Verify database file was created
	dbPath := filepath.Join(tmpDir, "test-namespace", "test-db.db")
	if _, err := os.Stat(dbPath); os.IsNotExist(err) {
		t.Errorf("Expected database file to be created at %s", dbPath)
	}

	// Verify response
	var resp map[string]interface{}
	json.NewDecoder(rr.Body).Decode(&resp)
	if resp["database_name"] != "test-db" {
		t.Errorf("Expected database_name 'test-db', got %v", resp["database_name"])
	}
}

// TestCreateDatabase_DuplicateName tests that duplicate database names are rejected
func TestCreateDatabase_DuplicateName(t *testing.T) {
	mockDB := &mockRQLiteClient{
		QueryFunc: func(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
			// For dns_nodes query
			if strings.Contains(query, "dns_nodes") {
				destValue := reflect.ValueOf(dest)
				if destValue.Kind() == reflect.Ptr {
					sliceValue := destValue.Elem()
					if sliceValue.Kind() == reflect.Slice {
						elemType := sliceValue.Type().Elem()
						newElem := reflect.New(elemType).Elem()
						idField := newElem.FieldByName("ID")
						if idField.IsValid() && idField.CanSet() {
							idField.SetString("node-test123")
						}
						sliceValue.Set(reflect.Append(sliceValue, newElem))
					}
				}
			}
			// For database check, return existing database
			if strings.Contains(query, "namespace_sqlite_databases") && strings.Contains(query, "SELECT") {
				destValue := reflect.ValueOf(dest)
				if destValue.Kind() == reflect.Ptr {
					sliceValue := destValue.Elem()
					if sliceValue.Kind() == reflect.Slice {
						elemType := sliceValue.Type().Elem()
						newElem := reflect.New(elemType).Elem()
						// Set ID field to indicate existing database
						idField := newElem.FieldByName("ID")
						if idField.IsValid() && idField.CanSet() {
							idField.SetString("existing-db-id")
						}
						sliceValue.Set(reflect.Append(sliceValue, newElem))
					}
				}
			}
			return nil
		},
	}

	tmpDir := t.TempDir()

	portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop())
	homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop())

	handler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop(), "", "")
	handler.basePath = tmpDir

	reqBody := map[string]string{
		"database_name": "test-db",
	}
	bodyBytes, _ := json.Marshal(reqBody)

	req := httptest.NewRequest("POST", "/v1/db/sqlite/create", bytes.NewReader(bodyBytes))
	ctx := context.WithValue(req.Context(), ctxkeys.NamespaceOverride, "test-namespace")
	req = req.WithContext(ctx)

	rr := httptest.NewRecorder()

	handler.CreateDatabase(rr, req)

	if rr.Code != http.StatusConflict {
		t.Errorf("Expected status 409 (Conflict), got %d", rr.Code)
	}
}

// TestCreateDatabase_InvalidName tests that invalid database names are rejected
func TestCreateDatabase_InvalidName(t *testing.T) {
	mockDB := &mockRQLiteClient{}
	tmpDir := t.TempDir()

	portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop())
	homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop())

	handler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop(), "", "")
	handler.basePath = tmpDir

	invalidNames := []string{
		"test db",       // Space
		"test@db",       // Special char
		"test/db",       // Slash
		"",              // Empty
		strings.Repeat("a", 100), // Too long
	}

	for _, name := range invalidNames {
		reqBody := map[string]string{
			"database_name": name,
		}
		bodyBytes, _ := json.Marshal(reqBody)

		req := httptest.NewRequest("POST", "/v1/db/sqlite/create", bytes.NewReader(bodyBytes))
		ctx := context.WithValue(req.Context(), ctxkeys.NamespaceOverride, "test-namespace")
		req = req.WithContext(ctx)

		rr := httptest.NewRecorder()

		handler.CreateDatabase(rr, req)

		if rr.Code != http.StatusBadRequest {
			t.Errorf("Expected status 400 for invalid name %q, got %d", name, rr.Code)
		}
	}
}

// TestListDatabases tests listing all databases for a namespace
func TestListDatabases(t *testing.T) {
	mockDB := &mockRQLiteClient{
		QueryFunc: func(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
			// Return empty list
			return nil
		},
	}

	portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop())
	homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop())

	handler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop(), "", "")

	req := httptest.NewRequest("GET", "/v1/db/sqlite/list", nil)
	ctx := context.WithValue(req.Context(), ctxkeys.NamespaceOverride, "test-namespace")
	req = req.WithContext(ctx)

	rr := httptest.NewRecorder()

	handler.ListDatabases(rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("Expected status 200, got %d", rr.Code)
	}

	var resp map[string]interface{}
	json.NewDecoder(rr.Body).Decode(&resp)

	if _, ok := resp["databases"]; !ok {
		t.Error("Expected 'databases' field in response")
	}

	if _, ok := resp["count"]; !ok {
		t.Error("Expected 'count' field in response")
	}
}

// TestBackupDatabase tests backing up a database to IPFS
func TestBackupDatabase(t *testing.T) {
	// Create a temporary database file
	tmpDir := t.TempDir()
	dbPath := filepath.Join(tmpDir, "test.db")

	// Create a real SQLite database
	db, err := sql.Open("sqlite3", dbPath)
	if err != nil {
		t.Fatalf("Failed to create test database: %v", err)
	}
	db.Exec("CREATE TABLE test (id INTEGER PRIMARY KEY)")
	db.Close()

	mockDB := &mockRQLiteClient{
		QueryFunc: func(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
			// Mock database record lookup - return struct with file_path
			if strings.Contains(query, "namespace_sqlite_databases") {
				destValue := reflect.ValueOf(dest)
				if destValue.Kind() == reflect.Ptr {
					sliceValue := destValue.Elem()
					if sliceValue.Kind() == reflect.Slice {
						elemType := sliceValue.Type().Elem()
						newElem := reflect.New(elemType).Elem()

						// Set fields
						idField := newElem.FieldByName("ID")
						if idField.IsValid() && idField.CanSet() {
							idField.SetString("test-db-id")
						}

						filePathField := newElem.FieldByName("FilePath")
						if filePathField.IsValid() && filePathField.CanSet() {
							filePathField.SetString(dbPath)
						}

						sliceValue.Set(reflect.Append(sliceValue, newElem))
					}
				}
			}
			return nil
		},
		ExecFunc: func(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
			return nil, nil
		},
	}

	mockIPFS := &mockIPFSClient{
		AddFunc: func(ctx context.Context, r io.Reader, filename string) (*ipfs.AddResponse, error) {
			// Verify data is being uploaded
			data, _ := io.ReadAll(r)
			if len(data) == 0 {
				t.Error("Expected non-empty database file upload")
			}
			return &ipfs.AddResponse{Cid: "QmBackupCID123"}, nil
		},
	}

	portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop())
	homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop())

	sqliteHandler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop(), "", "")

	backupHandler := NewBackupHandler(sqliteHandler, mockIPFS, zap.NewNop())

	reqBody := map[string]string{
		"database_name": "test-db",
	}
	bodyBytes, _ := json.Marshal(reqBody)

	req := httptest.NewRequest("POST", "/v1/db/sqlite/backup", bytes.NewReader(bodyBytes))
	ctx := context.WithValue(req.Context(), ctxkeys.NamespaceOverride, "test-namespace")
	req = req.WithContext(ctx)

	rr := httptest.NewRecorder()

	backupHandler.BackupDatabase(rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("Expected status 200, got %d", rr.Code)
		t.Logf("Response: %s", rr.Body.String())
	}

	var resp map[string]interface{}
	json.NewDecoder(rr.Body).Decode(&resp)

	if resp["backup_cid"] != "QmBackupCID123" {
		t.Errorf("Expected backup_cid 'QmBackupCID123', got %v", resp["backup_cid"])
	}
}

// TestIsValidDatabaseName tests database name validation
func TestIsValidDatabaseName(t *testing.T) {
	tests := []struct {
		name  string
		valid bool
	}{
		{"valid_db", true},
		{"valid-db", true},
		{"ValidDB123", true},
		{"test_db_123", true},
		{"test db", false},      // Space
		{"test@db", false},      // Special char
		{"test/db", false},      // Slash
		{"", false},             // Empty
		{strings.Repeat("a", 65), false}, // Too long
	}

	for _, tt := range tests {
		result := isValidDatabaseName(tt.name)
		if result != tt.valid {
			t.Errorf("isValidDatabaseName(%q) = %v, expected %v", tt.name, result, tt.valid)
		}
	}
}

// TestIsWriteQuery tests SQL query classification
func TestIsWriteQuery(t *testing.T) {
	tests := []struct {
		query   string
		isWrite bool
	}{
		{"SELECT * FROM users", false},
		{"INSERT INTO users VALUES (1, 'test')", true},
		{"UPDATE users SET name = 'test'", true},
		{"DELETE FROM users WHERE id = 1", true},
		{"CREATE TABLE test (id INT)", true},
		{"DROP TABLE test", true},
		{"ALTER TABLE test ADD COLUMN name TEXT", true},
		{"  insert into users values (1)", true}, // Case insensitive with whitespace
		{"select * from users", false},
	}

	for _, tt := range tests {
		result := isWriteQuery(tt.query)
		if result != tt.isWrite {
			t.Errorf("isWriteQuery(%q) = %v, expected %v", tt.query, result, tt.isWrite)
		}
	}
}
