package storage_test

import (
	"context"
	"os"
	"strings"
	"testing"
	"time"

	"git.toki-labs.com/toki/gito/services/core/internal/core"
	"git.toki-labs.com/toki/gito/services/core/internal/events"
	"git.toki-labs.com/toki/gito/services/core/internal/provider"
	"git.toki-labs.com/toki/gito/services/core/internal/storage"
)

// compile-time interface checks
var _ storage.Store = (*storage.Boundary)(nil)
var _ storage.BranchWatchStore = (*fakeBranchWatches)(nil)
var _ storage.RevisionCursorStore = (*fakeRevisionCursors)(nil)
var _ storage.ProviderDeliveryStore = (*fakeProviderDeliveries)(nil)
var _ storage.WebhookDeliveryStore = (*fakeWebhookDeliveries)(nil)
var _ storage.ProviderConfigStore = (*fakeProviderConfigs)(nil)

// fakes

type fakeWebhookDeliveries struct {
	records map[string]core.WebhookDelivery
}

func (f *fakeWebhookDeliveries) EnqueueDelivery(_ context.Context, delivery core.WebhookDelivery) (storage.WebhookDeliveryCreateResult, error) {
	if f.records == nil {
		f.records = make(map[string]core.WebhookDelivery)
	}
	key := delivery.EventID + ":" + delivery.SubscriptionID
	if existing, ok := f.records[key]; ok {
		return storage.WebhookDeliveryCreateResult{Delivery: existing, Created: false}, nil
	}
	if delivery.ID == "" {
		delivery.ID = "fake-delivery-id"
	}
	if delivery.Status == "" {
		delivery.Status = core.WebhookDeliveryPending
	}
	f.records[key] = delivery
	return storage.WebhookDeliveryCreateResult{Delivery: delivery, Created: true}, nil
}

func (f *fakeWebhookDeliveries) GetDelivery(_ context.Context, id string) (core.WebhookDelivery, error) {
	if f.records != nil {
		for _, d := range f.records {
			if d.ID == id {
				return d, nil
			}
		}
	}
	return core.WebhookDelivery{}, storage.ErrRepoNotFound // Return a storage error type
}

func (f *fakeWebhookDeliveries) PickPendingDeliveries(_ context.Context, limit int, now time.Time) ([]core.WebhookDelivery, error) {
	var list []core.WebhookDelivery
	if f.records != nil {
		for _, d := range f.records {
			if (d.Status == core.WebhookDeliveryPending || d.Status == core.WebhookDeliveryRetryable) && (d.NextAttemptAt.Before(now) || d.NextAttemptAt.Equal(now)) {
				d.Status = core.WebhookDeliverySending
				f.records[d.EventID+":"+d.SubscriptionID] = d
				list = append(list, d)
				if len(list) >= limit {
					break
				}
			}
		}
	}
	return list, nil
}

func (f *fakeWebhookDeliveries) StartDelivery(_ context.Context, id string, now time.Time) (core.WebhookDelivery, bool, error) {
	if f.records != nil {
		for key, d := range f.records {
			if d.ID == id && (d.Status == core.WebhookDeliveryPending || d.Status == core.WebhookDeliveryRetryable) {
				d.Status = core.WebhookDeliverySending
				f.records[key] = d
				return d, true, nil
			}
		}
	}
	return core.WebhookDelivery{}, false, nil
}

func (f *fakeWebhookDeliveries) RecordOutcome(_ context.Context, id string, status core.WebhookDeliveryStatus, statusCode int, lastErr string, nextAttemptAt time.Time, now time.Time) (core.WebhookDelivery, error) {
	if f.records != nil {
		for key, d := range f.records {
			if d.ID == id {
				d.Status = status
				d.LastStatusCode = statusCode
				d.LastError = lastErr
				d.NextAttemptAt = nextAttemptAt
				d.AttemptCount++
				f.records[key] = d
				return d, nil
			}
		}
	}
	return core.WebhookDelivery{}, storage.ErrRepoNotFound
}

type fakeRepos struct{}

func (f *fakeRepos) CreateRepo(_ context.Context, _ core.Repo) error        { return nil }
func (f *fakeRepos) GetRepo(_ context.Context, _ string) (core.Repo, error) { return core.Repo{}, nil }
func (f *fakeRepos) ListRepos(_ context.Context) ([]core.Repo, error)       { return nil, nil }
func (f *fakeRepos) UpdateRepo(_ context.Context, _ core.Repo) error        { return nil }

var _ storage.RepoStore = (*fakeRepos)(nil)

type fakeLeases struct{}

func (f *fakeLeases) SaveLease(_ context.Context, _ core.WorkspaceLease) error { return nil }
func (f *fakeLeases) GetLease(_ context.Context, _ string) (core.WorkspaceLease, error) {
	return core.WorkspaceLease{}, nil
}
func (f *fakeLeases) ListLeases(_ context.Context, _ string) ([]core.WorkspaceLease, error) {
	return nil, nil
}
func (f *fakeLeases) AcquireAvailableLease(_ context.Context, _ string, _ time.Time, _ time.Duration) (core.WorkspaceLease, bool, error) {
	return core.WorkspaceLease{}, false, nil
}
func (f *fakeLeases) ReleaseLease(_ context.Context, _ string, _ storage.LeaseRelease) (core.WorkspaceLease, error) {
	return core.WorkspaceLease{}, nil
}

var _ storage.WorkspaceLeaseStore = (*fakeLeases)(nil)

type fakeOps struct{}

func (f *fakeOps) CreateOperation(_ context.Context, op core.Operation) (storage.OperationCreateResult, error) {
	return storage.OperationCreateResult{Operation: op, Created: true}, nil
}
func (f *fakeOps) GetOperation(_ context.Context, _ string) (core.Operation, error) {
	return core.Operation{}, nil
}
func (f *fakeOps) ListOperations(_ context.Context, _ string) ([]core.Operation, error) {
	return nil, nil
}
func (f *fakeOps) StartOperation(_ context.Context, _ string, _ time.Time) (core.Operation, error) {
	return core.Operation{}, nil
}
func (f *fakeOps) SucceedOperation(_ context.Context, _ string, _ time.Time) (core.Operation, error) {
	return core.Operation{}, nil
}
func (f *fakeOps) FailOperation(_ context.Context, _ string, _ time.Time) (core.Operation, error) {
	return core.Operation{}, nil
}
func (f *fakeOps) CancelOperation(_ context.Context, _ string, _ time.Time) (core.Operation, error) {
	return core.Operation{}, nil
}
func (f *fakeOps) PickQueuedOperation(_ context.Context, _ time.Time) (core.Operation, bool, error) {
	return core.Operation{}, false, nil
}
func (f *fakeOps) PickQueuedAgentRunOperation(_ context.Context, _ time.Time) (core.Operation, bool, error) {
	return core.Operation{}, false, nil
}

var _ storage.OperationStore = (*fakeOps)(nil)

type fakeOpEvents struct{}

func (f *fakeOpEvents) AppendEvent(_ context.Context, _ storage.OperationEvent) error { return nil }
func (f *fakeOpEvents) ListEvents(_ context.Context, _ string) ([]storage.OperationEvent, error) {
	return nil, nil
}
func (f *fakeOpEvents) ListPendingEvents(_ context.Context, _ int) ([]storage.OperationEvent, error) {
	return nil, nil
}
func (f *fakeOpEvents) MarkPublished(_ context.Context, _ string, _ time.Time) (storage.OperationEvent, error) {
	return storage.OperationEvent{}, nil
}

var _ storage.OperationEventStore = (*fakeOpEvents)(nil)

// OperationEvent uses events.Event
var _ = storage.OperationEvent{Event: events.Event{}}

type fakeBranchWatches struct{}

func (f *fakeBranchWatches) UpsertBranchWatch(_ context.Context, watch core.BranchWatch) (core.BranchWatch, error) {
	return watch, nil
}
func (f *fakeBranchWatches) FindBranchWatch(_ context.Context, _, _, _ string) (core.BranchWatch, bool, error) {
	return core.BranchWatch{}, false, nil
}
func (f *fakeBranchWatches) ListBranchWatches(_ context.Context) ([]core.BranchWatch, error) {
	return nil, nil
}

type fakeRevisionCursors struct{}

func (f *fakeRevisionCursors) UpsertRevisionCursor(_ context.Context, _ core.RevisionCursor) error {
	return nil
}
func (f *fakeRevisionCursors) GetRevisionCursor(_ context.Context, _, _ string) (core.RevisionCursor, bool, error) {
	return core.RevisionCursor{}, false, nil
}

type fakeProviderDeliveries struct {
	records map[string]core.ProviderDelivery
}

func (f *fakeProviderDeliveries) RecordOnce(_ context.Context, delivery core.ProviderDelivery) (storage.DeliveryResult, error) {
	if f.records == nil {
		f.records = make(map[string]core.ProviderDelivery)
	}
	if existing, ok := f.records[delivery.DedupeKey]; ok {
		return storage.DeliveryResult{
			First:             false,
			ExistingEventID:   existing.EventID,
			ExistingCreatedAt: existing.CreatedAt,
		}, nil
	}
	f.records[delivery.DedupeKey] = delivery
	return storage.DeliveryResult{First: true}, nil
}

func (f *fakeProviderDeliveries) DeleteDelivery(_ context.Context, provider, dedupeKey string) error {
	if f.records != nil {
		delete(f.records, dedupeKey)
	}
	return nil
}

type fakeProviderConfigs struct {
	records map[provider.ProviderID]provider.Config
}

func (f *fakeProviderConfigs) SaveProviderConfig(_ context.Context, config provider.Config) (provider.Config, error) {
	if f.records == nil {
		f.records = make(map[provider.ProviderID]provider.Config)
	}
	if config.ID == "" {
		return provider.Config{}, storage.ErrInvalidProviderConfig
	}
	f.records[config.ID] = config
	return config, nil
}

func (f *fakeProviderConfigs) GetProviderConfig(_ context.Context, id provider.ProviderID) (provider.Config, bool, error) {
	if f.records != nil {
		if cfg, ok := f.records[id]; ok {
			return cfg, true, nil
		}
	}
	return provider.Config{}, false, nil
}

func (f *fakeProviderConfigs) ListProviderConfigs(_ context.Context) ([]provider.Config, error) {
	if f.records == nil {
		return nil, nil
	}
	result := make([]provider.Config, 0, len(f.records))
	for _, cfg := range f.records {
		result = append(result, cfg)
	}
	return result, nil
}

// ensure time import is used
var _ = time.Time{}

func newBoundary() *storage.Boundary {
	return storage.New(&fakeRepos{}, &fakeLeases{}, &fakeOps{}, &fakeOpEvents{})
}

func TestBoundaryContract(t *testing.T) {
	repos := &fakeRepos{}
	leases := &fakeLeases{}
	ops := &fakeOps{}
	opEvents := &fakeOpEvents{}

	b := storage.New(repos, leases, ops, opEvents)

	if b.Repos() != repos {
		t.Error("Repos() must return the injected repo store")
	}
	if b.WorkspaceLeases() != leases {
		t.Error("WorkspaceLeases() must return the injected lease store")
	}
	if b.Operations() != ops {
		t.Error("Operations() must return the injected operation store")
	}
	if b.OperationEvents() != opEvents {
		t.Error("OperationEvents() must return the injected operation event store")
	}

	if err := b.Ping(context.Background()); err != nil {
		t.Errorf("Ping with fully injected boundary: %v", err)
	}
}

func TestBoundaryProviderConfigsAccessor(t *testing.T) {
	repos := &fakeRepos{}
	leases := &fakeLeases{}
	ops := &fakeOps{}
	opEvents := &fakeOpEvents{}
	providerConfigs := &fakeProviderConfigs{}

	b := storage.NewWithProviderConfigs(repos, leases, ops, opEvents, providerConfigs)

	if b.ProviderConfigs() != providerConfigs {
		t.Error("ProviderConfigs() must return the injected provider config store")
	}

	// Verify Store interface includes ProviderConfigs
	var _ storage.Store = b
	if b.ProviderConfigs() != providerConfigs {
		t.Error("Store.ProviderConfigs() must return the injected provider config store")
	}
}

func TestProviderConfigStoreMethods(t *testing.T) {
	fake := &fakeProviderConfigs{}

	// SaveProviderConfig with empty ID should fail
	_, err := fake.SaveProviderConfig(context.Background(), provider.Config{})
	if err != storage.ErrInvalidProviderConfig {
		t.Errorf("expected ErrInvalidProviderConfig for empty ID, got %v", err)
	}

	// SaveProviderConfig with valid ID
	cfg := provider.Config{
		ID:       provider.ProviderGitHub,
		Endpoint: "https://github.com",
	}
	saved, err := fake.SaveProviderConfig(context.Background(), cfg)
	if err != nil {
		t.Fatalf("SaveProviderConfig failed: %v", err)
	}
	if saved.ID != provider.ProviderGitHub {
		t.Errorf("expected provider ID %q, got %q", provider.ProviderGitHub, saved.ID)
	}

	// GetProviderConfig should find the saved config
	found, ok, err := fake.GetProviderConfig(context.Background(), provider.ProviderGitHub)
	if err != nil {
		t.Fatalf("GetProviderConfig failed: %v", err)
	}
	if !ok {
		t.Error("GetProviderConfig should return found=true for existing config")
	}
	if found.ID != provider.ProviderGitHub {
		t.Errorf("expected provider ID %q, got %q", provider.ProviderGitHub, found.ID)
	}

	// GetProviderConfig for non-existing ID
	_, ok, err = fake.GetProviderConfig(context.Background(), provider.ProviderID("nonexistent"))
	if err != nil {
		t.Fatalf("GetProviderConfig failed: %v", err)
	}
	if ok {
		t.Error("GetProviderConfig should return found=false for non-existing config")
	}

	// ListProviderConfigs should return all configs
	configs, err := fake.ListProviderConfigs(context.Background())
	if err != nil {
		t.Fatalf("ListProviderConfigs failed: %v", err)
	}
	if len(configs) != 1 {
		t.Errorf("expected 1 config, got %d", len(configs))
	}
}

func TestBoundaryPingNilContext(t *testing.T) {
	b := newBoundary()
	//nolint:staticcheck
	if err := b.Ping(nil); err == nil {
		t.Error("Ping with nil context must return error")
	}
}

func TestBoundaryPingMissingSubStore(t *testing.T) {
	b := storage.New(nil, nil, nil, nil)
	if err := b.Ping(context.Background()); err == nil {
		t.Error("Ping with nil sub-stores must return error")
	}
}

func TestInitialMigrationSupportsStoreBoundary(t *testing.T) {
	data, err := os.ReadFile("../../migrations/00001_initial.sql")
	if err != nil {
		t.Fatalf("cannot read migration file: %v", err)
	}
	content := string(data)

	tableSnippets := []string{
		"CREATE TABLE IF NOT EXISTS repos",
		"CREATE TABLE IF NOT EXISTS workspace_leases",
		"CREATE TABLE IF NOT EXISTS operations",
		"CREATE TABLE IF NOT EXISTS operation_events",
		"CREATE TABLE IF NOT EXISTS revision_cursors",
		"CREATE TABLE IF NOT EXISTS branch_watches",
		"CREATE TABLE IF NOT EXISTS provider_deliveries",
		"CREATE TABLE IF NOT EXISTS provider_configs",
		"CREATE TABLE IF NOT EXISTS agent_run_inputs",
		"CREATE TABLE IF NOT EXISTS webhook_deliveries",
	}

	// required column/constraint snippets
	columnSnippets := []string{
		"remote_url      TEXT NOT NULL",
		"default_branch  TEXT NOT NULL",
		"workspace_root  TEXT NOT NULL",
		"credential_ref  TEXT",
		"state       TEXT NOT NULL",
		"expires_at  TIMESTAMPTZ",
		"UNIQUE (repo_id, slot)",
		"type             TEXT NOT NULL CHECK (type IN ('clone', 'fetch', 'commit', 'push', 'agent_run'))",
		"state            TEXT NOT NULL CHECK (state IN ('queued', 'running', 'succeeded', 'failed', 'cancelled'))",
		"idempotency_key  TEXT CHECK (idempotency_key IS NULL OR btrim(idempotency_key) <> '')",
		"operation_id",
		"ALTER TABLE revision_cursors DROP CONSTRAINT IF EXISTS revision_cursors_repo_id_fkey",
		"UNIQUE (provider, repo_id, branch)",
		"UNIQUE (provider, dedupe_key)",
		"published_at  TIMESTAMPTZ",
		"CREATE INDEX IF NOT EXISTS ix_operation_events_operation_ordering",
		"CREATE INDEX IF NOT EXISTS ix_operation_events_pending_ordering",
		"UNIQUE (event_id, subscription_id)",
		"CREATE INDEX IF NOT EXISTS ix_webhook_deliveries_pending",
		// provider_configs columns
		"CREATE TABLE IF NOT EXISTS provider_configs",
		"endpoint        TEXT NOT NULL CHECK (btrim(endpoint) <> '')",
		"credential_ref  TEXT",
		"capabilities    BIGINT NOT NULL CHECK (capabilities >= 0)",
	}
	forbiddenSnippets := []string{
		"CREATE TABLE IF NOT EXISTS revision_cursors (\n    repo_id      TEXT NOT NULL REFERENCES repos(id)",
	}

	for _, snippet := range tableSnippets {
		if !strings.Contains(content, snippet) {
			t.Errorf("migration missing required table definition: %q", snippet)
		}
	}
	for _, col := range columnSnippets {
		if !strings.Contains(content, col) {
			t.Errorf("migration missing required column snippet: %q", col)
		}
	}
	for _, snippet := range forbiddenSnippets {
		if strings.Contains(content, snippet) {
			t.Errorf("migration contains forbidden revision cursor FK snippet: %q", snippet)
		}
	}
}
