package metacache

import (
	"context"
	"errors"
	"io"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	o11yv1 "github.com/tidbcloud/observability/api/regionalserver/resourceworker/v1"
	"github.com/tidbcloud/observability/o11ydiagnosis/pkg/storage"
)

// MockStorage is a mock implementation of the storage.Storage interface
type MockStorage struct {
	mock.Mock
}

func (m *MockStorage) ListPrefixObject(ctx context.Context, bucket string, prefix string) ([]storage.ObjectMeta, error) {
	//TODO implement me
	panic("implement me")
}

func (m *MockStorage) ListObject(ctx context.Context, bucket string) ([]storage.ObjectMeta, error) {
	//TODO implement me
	panic("implement me")
}

func (m *MockStorage) ListObjectsPaged(ctx context.Context, bucket string, prefix string, pageSize int, pageToken string) (objects []storage.ObjectMeta, nextPageToken string, err error) {
	//TODO implement me
	panic("implement me")
}

func (m *MockStorage) Mkdir(ctx context.Context, bucket, key string, body io.Reader) error {
	//TODO implement me
	panic("implement me")
}

func (m *MockStorage) Object(ctx context.Context, bucket, key string) (*storage.Object, error) {
	args := m.Called(ctx, bucket, key)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*storage.Object), args.Error(1)
}

func (m *MockStorage) Objects(ctx context.Context, bucket, prefix string) ([]storage.ObjectMeta, error) {
	args := m.Called(ctx, bucket, prefix)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]storage.ObjectMeta), args.Error(1)
}

func (m *MockStorage) ObjectsCallback(ctx context.Context, bucket, prefix string, callback func(storage.ObjectMeta) bool) error {
	args := m.Called(ctx, bucket, prefix, callback)
	return args.Error(0)
}

func (m *MockStorage) ObjectsCallbackWithStartKey(ctx context.Context, bucket, prefix, startKey string, callback func(storage.ObjectMeta) bool) error {
	args := m.Called(ctx, bucket, prefix, startKey, callback)
	return args.Error(0)
}

func (m *MockStorage) ObjectsDelimiter(ctx context.Context, bucket, prefix string) ([]string, error) {
	args := m.Called(ctx, bucket, prefix)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]string), args.Error(1)
}

func (m *MockStorage) SaveObject(ctx context.Context, bucket, key string, body io.Reader) error {
	args := m.Called(ctx, bucket, key, body)
	return args.Error(0)
}

func (m *MockStorage) DeleteObject(ctx context.Context, bucket, key string) error {
	args := m.Called(ctx, bucket, key)
	return args.Error(0)
}

func (m *MockStorage) DeleteObjects(ctx context.Context, bucket string, keys []string) error {
	args := m.Called(ctx, bucket, keys)
	return args.Error(0)
}

func (m *MockStorage) ObjectExists(ctx context.Context, bucket, key string) (bool, error) {
	args := m.Called(ctx, bucket, key)
	return args.Bool(0), args.Error(1)
}

func (m *MockStorage) ObjectSize(ctx context.Context, bucket, key string) (int64, error) {
	args := m.Called(ctx, bucket, key)
	return args.Get(0).(int64), args.Error(1)
}

// Add other required storage.Storage interface methods with empty implementations
func (m *MockStorage) PutObject(ctx context.Context, bucket, key string, data []byte) error {
	return nil
}

func (m *MockStorage) ListObjects(ctx context.Context, bucket, prefix string) ([]string, error) {
	return nil, nil
}

func TestMetaCache_GetMetaInfo(t *testing.T) {
	// Setup test cases
	tests := []struct {
		name          string
		env           string
		region        string
		tenant        string
		project       string
		cluster       string
		nextGen       bool
		setupMocks    func(*testing.T, *MockStorage, client.Client)
		expected      *MetaInfo
		expectedError bool
	}{
		{
			name:    "successful get from cache",
			env:     "test",
			region:  "us-west-2",
			tenant:  "test-tenant",
			project: "test-project",
			cluster: "test-cluster",
			nextGen: false,
			setupMocks: func(t *testing.T, s *MockStorage, k8s client.Client) {
				// No mocks needed as it should hit cache first
			},
			expected: &MetaInfo{
				O11yID:      "cached-id",
				VMAccountID: 123,
				VMProjectID: 456,
			},
		},
		{
			name:    "successful get from kubernetes",
			env:     "test",
			region:  "us-west-2",
			tenant:  "test-tenant",
			project: "test-project",
			cluster: "test-cluster",
			nextGen: false,
			setupMocks: func(t *testing.T, s *MockStorage, k8s client.Client) {
				// Create a test cluster in the fake client
				cluster := &o11yv1.Cluster{
					Spec: o11yv1.ClusterSpec{
						VictoriaAccount: o11yv1.VictoriaAccount{
							AccountID: 789,
							ProjectID: 101,
						},
					},
				}
				cluster.Name = "test-cluster-id"
				err := k8s.Create(context.Background(), cluster)
				assert.NoError(t, err)
			},
			expected: &MetaInfo{
				O11yID:      "test-cluster-id",
				VMAccountID: 789,
				VMProjectID: 101,
			},
		},
		{
			name:    "successful get from storage",
			env:     "prod",
			region:  "ap-northeast-1",
			tenant:  "1372813089209265136",
			project: "1372813089454582860",
			cluster: "test-cluster",
			nextGen: false,
			setupMocks: func(t *testing.T, s *MockStorage, k8s client.Client) {
				// Mock the storage to return a YAML file
				yamlContent := `
clusterbaseinfo:
  id: 34750647-d0ba-4e59-9ffe-cd719c1c6af6
vmaccountid: "6677"
vmprojectid: "8899"
`
				s.On("Object", mock.Anything, "o11y-prod-shared-ap-northeast-1", "archived/tidbcloud/1372813089209265136/1372813089454582860/test-cluster/cluster.yaml").
					Return(&storage.Object{
						Reader: &mockReader{data: []byte(yamlContent)},
					}, nil)
			},
			expected: &MetaInfo{
				O11yID:      "34750647-d0ba-4e59-9ffe-cd719c1c6af6",
				VMAccountID: 6677,
				VMProjectID: 8899,
			},
		},
		{
			name:    "not found in storage",
			env:     "prod",
			region:  "ap-northeast-1",
			tenant:  "test-tenant",
			project: "test-project",
			cluster: "non-existent",
			nextGen: false,
			setupMocks: func(t *testing.T, s *MockStorage, k8s client.Client) {
				s.On("Object", mock.Anything, mock.Anything, mock.Anything).
					Return(nil, storage.ErrNotFound)
			},
			expected:      nil,
			expectedError: false,
		},
		{
			name:    "storage error",
			env:     "prod",
			region:  "ap-northeast-1",
			tenant:  "test-tenant",
			project: "test-project",
			cluster: "error-cluster",
			nextGen: false,
			setupMocks: func(t *testing.T, s *MockStorage, k8s client.Client) {
				s.On("Object", mock.Anything, mock.Anything, mock.Anything).
					Return(nil, errors.New("storage error"))
			},
			expected:      nil,
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Setup
			scheme := runtime.NewScheme()
			err := o11yv1.AddToScheme(scheme)
			if err != nil {
				t.Fatalf("Failed to add scheme: %v", err)
			}
			k8sClient := fake.NewClientBuilder().WithScheme(scheme).Build()
			mockStorage := new(MockStorage)

			// Setup mocks
			tt.setupMocks(t, mockStorage, k8sClient)

			// Create cache with test data if needed
			cache := NewMetaCache(k8sClient, mockStorage).(*metaCache)
			if tt.expected != nil && tt.name == "successful get from cache" {
				cache.cache.Store(tt.cluster, tt.expected)
			}

			// Execute
			result, err := cache.GetMetaInfo(
				context.Background(),
				tt.env,
				tt.region,
				tt.tenant,
				tt.project,
				tt.cluster,
				tt.nextGen,
				false,
			)

			// Verify
			if tt.expectedError {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}

			assert.Equal(t, tt.expected, result)

			// Verify all expectations were met
			mockStorage.AssertExpectations(t)
		})
	}
}

// mockReader is a simple io.ReadCloser for testing
type mockReader struct {
	data   []byte
	offset int
}

func (m *mockReader) Read(p []byte) (n int, err error) {
	if m.offset >= len(m.data) {
		return 0, io.EOF
	}
	n = copy(p, m.data[m.offset:])
	m.offset += n
	return n, nil
}

func (m *mockReader) Close() error {
	return nil
}
