package pipeline

import (
	"context"
	"errors"
	"io"
	"runtime"
	"testing"
	"time"

	"git.tswf.io/infra/go-synapse-backupper/pkg/domain"
	"git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
	"git.tswf.io/infra/go-synapse-backupper/pkg/domain/pgdump"
)

var (
	errPgDumpFailed  = errors.New("pg_dump failed")
	errEncryptFailed = errors.New("encryption failed")
)

type fakeSink struct {
	transaction *fakeSinkTx
}

func (sink *fakeSink) Begin(key string) (domain.SinkTx, error) {
	sink.transaction = &fakeSinkTx{}
	return sink.transaction, nil
}

func (sink *fakeSink) List(prefix string) ([]string, error) {
	return make([]string, 0), nil
}

func (sink *fakeSink) Remove(key string) error {
	return nil
}

type fakeSinkTx struct {
	committed bool
	aborted   bool
	data      []byte
}

func (transaction *fakeSinkTx) Write(p []byte) (int, error) {
	transaction.data = append(transaction.data, p...)
	return len(p), nil
}

func (transaction *fakeSinkTx) Commit() error {
	transaction.committed = true
	return nil
}

func (transaction *fakeSinkTx) Abort() error {
	transaction.aborted = true
	return nil
}

type fakeDumper struct {
	writeBytes int
	returnErr  error
	closePipe  bool
}

func (dumper *fakeDumper) Dump(
	ctx context.Context,
	opts pgdump.Options,
	writer io.Writer,
) error {
	if dumper.writeBytes > 0 {
		data := make([]byte, dumper.writeBytes)
		if _, err := writer.Write(data); err != nil {
			return err
		}
	}
	if dumper.closePipe {
		if closer, ok := writer.(io.Closer); ok {
			_ = closer.Close()
		}
	}
	return dumper.returnErr
}

type fakeEncryptor struct {
	readBytes int
	returnErr error
}

func (encryptor *fakeEncryptor) Encrypt(
	plaintext io.Reader,
	recipients []crypto.RecipientPub,
	sink io.Writer,
	rand io.Reader,
) error {
	if encryptor.readBytes > 0 {
		buf := make([]byte, encryptor.readBytes)
		if _, err := io.ReadFull(plaintext, buf); err != nil {
			return err
		}
	}
	return encryptor.returnErr
}

func countGoroutines() int {
	return runtime.NumGoroutine()
}

func waitForGoroutinesStable(baseline int) bool {
	deadline := time.Now().Add(time.Second)
	for time.Now().Before(deadline) {
		time.Sleep(10 * time.Millisecond)
		if runtime.NumGoroutine() <= baseline {
			return true
		}
	}
	return false
}

func TestPipeline_DumpReturnsWithoutClosing(t *testing.T) {
	baseline := countGoroutines()

	sink := &fakeSink{}
	dumper := &fakeDumper{
		writeBytes: 4 * 1024,
		returnErr:  errPgDumpFailed,
		closePipe:  false,
	}
	encryptor := &fakeEncryptor{
		readBytes: 4 * 1024,
	}

	runner := NewRunner(
		WithDumper(dumper),
		WithEncryptor(encryptor),
	)

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	err := runner.Run(
		ctx,
		pgdump.Options{Key: "backup.sql"},
		make([]crypto.RecipientPub, 0),
		sink,
		nil,
	)

	if !errors.Is(err, errPgDumpFailed) {
		t.Fatalf("expected errPgDumpFailed, got %v", err)
	}

	if !sink.transaction.aborted {
		t.Fatalf("expected transaction to be aborted on error")
	}

	if waitForGoroutinesStable(baseline) {
		return
	}

	t.Fatalf("goroutine leak detected: baseline %d, current %d", baseline, countGoroutines())
}

func TestPipeline_EncryptFailsFirst(t *testing.T) {
	baseline := countGoroutines()

	sink := &fakeSink{}
	dumper := &fakeDumper{
		writeBytes: 64 * 1024,
		returnErr:  nil,
		closePipe:  false,
	}
	encryptor := &fakeEncryptor{
		readBytes: 1024,
		returnErr: errEncryptFailed,
	}

	runner := NewRunner(
		WithDumper(dumper),
		WithEncryptor(encryptor),
	)

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	err := runner.Run(
		ctx,
		pgdump.Options{Key: "backup.sql"},
		make([]crypto.RecipientPub, 0),
		sink,
		nil,
	)

	if !errors.Is(err, errEncryptFailed) {
		t.Fatalf("expected errEncryptFailed, got %v", err)
	}

	if !sink.transaction.aborted {
		t.Fatalf("expected transaction to be aborted on error")
	}

	if waitForGoroutinesStable(baseline) {
		return
	}

	t.Fatalf("goroutine leak detected: baseline %d, current %d", baseline, countGoroutines())
}

func TestPipeline_SuccessfulRunCommits(t *testing.T) {
	baseline := countGoroutines()

	sink := &fakeSink{}
	dumper := &fakeDumper{
		writeBytes: 4 * 1024,
		returnErr:  nil,
		closePipe:  true,
	}
	encryptor := &fakeEncryptor{
		readBytes: 4 * 1024,
	}

	runner := NewRunner(
		WithDumper(dumper),
		WithEncryptor(encryptor),
	)

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	err := runner.Run(
		ctx,
		pgdump.Options{Key: "backup.sql"},
		make([]crypto.RecipientPub, 0),
		sink,
		nil,
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if !sink.transaction.committed {
		t.Fatalf("expected transaction to be committed on success")
	}

	if waitForGoroutinesStable(baseline) {
		return
	}

	t.Fatalf("goroutine leak detected: baseline %d, current %d", baseline, countGoroutines())
}
