package main

import (
	"bytes"
	"context"
	"io"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"testing"
	"time"

	"github.com/spf13/cobra"

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

// --- Mocks ---

type mockRecipientPub struct {
	schemeID uint16
	keyID    []byte
	raw      []byte
}

func (m *mockRecipientPub) SchemeID() uint16 { return m.schemeID }
func (m *mockRecipientPub) KeyID() []byte    { return m.keyID }
func (m *mockRecipientPub) Raw() []byte      { return m.raw }

type mockKeyManager struct {
	loadPubCalls []loadPubCall
	pub          crypto.RecipientPub
}

type loadPubCall struct {
	path     string
	schemeID uint16
}

func (m *mockKeyManager) LoadPub(
	path string,
	schemeID uint16,
) (
	crypto.RecipientPub,
	error,
) {
	m.loadPubCalls = append(
		m.loadPubCalls,
		loadPubCall{path: path, schemeID: schemeID},
	)
	return m.pub, nil
}

func (m *mockKeyManager) LoadPriv(
	path string,
	schemeID uint16,
) (
	crypto.RecipientPriv,
	error,
) {
	return nil, nil
}

func (m *mockKeyManager) Generate(
	schemeID uint16,
	pubOut io.Writer,
	privOut io.Writer,
	rand io.Reader,
) error {
	return nil
}

type successDumper struct {
	data         []byte
	receivedOpts pgdump.Options
}

func (d *successDumper) Dump(
	ctx context.Context,
	opts pgdump.Options,
	sink io.Writer,
) error {
	d.receivedOpts = opts
	if len(d.data) > 0 {
		_, err := sink.Write(d.data)
		if err != nil {
			return err
		}
	}
	if closer, ok := sink.(io.Closer); ok {
		_ = closer.Close()
	}
	return nil
}

type failDumper struct{}

func (d *failDumper) Dump(
	ctx context.Context,
	opts pgdump.Options,
	sink io.Writer,
) error {
	return pgdump.ErrPgDumpFailed(1)
}

type passthroughEncryptor struct{}

func (e *passthroughEncryptor) Encrypt(
	plaintext io.Reader,
	recipients []crypto.RecipientPub,
	sink io.Writer,
	rand io.Reader,
) error {
	_, err := io.Copy(sink, plaintext)
	return err
}

// --- Helpers ---

func setBackupFlags(
	cmd *cobra.Command,
	backupDir string,
	pqPath string,
	classicalPath string,
) {
	config.RegisterFlags(cmd)
	_ = cmd.Flags().Set("backup-dir", backupDir)
	_ = cmd.Flags().Set("pq-public-key-path", pqPath)
	_ = cmd.Flags().Set("classical-public-key-path", classicalPath)
	_ = cmd.Flags().Set("pg-host", "localhost")
	_ = cmd.Flags().Set("pg-port", "5432")
	_ = cmd.Flags().Set("pg-user", "testuser")
	_ = cmd.Flags().Set("pg-database", "testdb")
}

func restoreGlobals(t *testing.T) {
	originalNewKeyManager := newKeyManager
	originalNewRunner := newRunner
	originalOutputWriter := outputWriter
	t.Cleanup(func() {
		newKeyManager = originalNewKeyManager
		newRunner = originalNewRunner
		outputWriter = originalOutputWriter
	})
}

// --- Tests ---

func TestBackupCmd_Structure(t *testing.T) {
	if backupCmd == nil {
		t.Fatal("backupCmd is nil")
	}
	if backupCmd.Use != "backup" {
		t.Fatalf("expected Use='backup', got %q", backupCmd.Use)
	}
}

func TestBackupCmd_Success(t *testing.T) {
	restoreGlobals(t)
	t.Setenv("APP_PG_PASSWORD", "testpass")

	backupDir := t.TempDir()
	pqPath := filepath.Join(backupDir, "pq.pub")
	classicalPath := filepath.Join(backupDir, "classical.pub")

	_ = os.WriteFile(pqPath, []byte("pq"), 0o644)
	_ = os.WriteFile(classicalPath, []byte("classical"), 0o644)

	mockKM := &mockKeyManager{
		pub: &mockRecipientPub{
			schemeID: 0x0006,
			keyID:    make([]byte, 8),
			raw:      make([]byte, 32),
		},
	}
	newKeyManager = func(crypto.Registry) crypto.KeyManager {
		return mockKM
	}

	testData := []byte("test backup payload")
	dumper := &successDumper{data: testData}
	newRunner = func(...pipeline.Option) pipeline.Runner {
		return pipeline.NewRunner(
			pipeline.WithDumper(dumper),
			pipeline.WithEncryptor(&passthroughEncryptor{}),
		)
	}

	var buf bytes.Buffer
	outputWriter = &buf

	cmd := &cobra.Command{}
	setBackupFlags(cmd, backupDir, pqPath, classicalPath)
	_ = cmd.Flags().Set("backup-retention-days", "1")

	oldFile := filepath.Join(backupDir, "synapse-20230101-000000.dump.pqenc")
	_ = os.WriteFile(oldFile, []byte("old"), 0o644)
	oldTime := time.Now().Add(-48 * time.Hour)
	_ = os.Chtimes(oldFile, oldTime, oldTime)

	err := backupCmd.RunE(cmd, []string{})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if len(mockKM.loadPubCalls) != 2 {
		t.Fatalf("expected 2 LoadPub calls, got %d", len(mockKM.loadPubCalls))
	}
	if mockKM.loadPubCalls[0].path != pqPath ||
		mockKM.loadPubCalls[0].schemeID != 0x0006 {
		t.Fatalf("unexpected PQ LoadPub call: %+v", mockKM.loadPubCalls[0])
	}
	if mockKM.loadPubCalls[1].path != classicalPath ||
		mockKM.loadPubCalls[1].schemeID != 0x0007 {
		t.Fatalf(
			"unexpected classical LoadPub call: %+v",
			mockKM.loadPubCalls[1],
		)
	}

	if dumper.receivedOpts.Host != "localhost" {
		t.Fatalf("unexpected host: %q", dumper.receivedOpts.Host)
	}
	if dumper.receivedOpts.Port != 5432 {
		t.Fatalf("unexpected port: %d", dumper.receivedOpts.Port)
	}
	if dumper.receivedOpts.Database != "testdb" {
		t.Fatalf("unexpected database: %q", dumper.receivedOpts.Database)
	}
	if dumper.receivedOpts.User != "testuser" {
		t.Fatalf("unexpected user: %q", dumper.receivedOpts.User)
	}
	if dumper.receivedOpts.Password != "testpass" {
		t.Fatalf("unexpected password: %q", dumper.receivedOpts.Password)
	}

	key := dumper.receivedOpts.Key
	matched, _ := regexp.MatchString(
		`^synapse-\d{8}-\d{6}-[a-f0-9]{6}\.dump\.pqenc$`,
		key,
	)
	if !matched {
		t.Fatalf("unexpected key format: %q", key)
	}

	entries, err := os.ReadDir(backupDir)
	if err != nil {
		t.Fatal(err)
	}
	var foundFinal int
	var foundTmp int
	for _, entry := range entries {
		name := entry.Name()
		if strings.HasSuffix(name, ".dump.pqenc") {
			foundFinal++
		}
		if strings.HasSuffix(name, ".tmp") {
			foundTmp++
		}
	}
	if foundFinal != 1 {
		t.Fatalf("expected 1 final .pqenc file, found %d", foundFinal)
	}
	if foundTmp != 0 {
		t.Fatalf("expected 0 .tmp files, found %d", foundTmp)
	}

	if _, err := os.Stat(oldFile); !os.IsNotExist(err) {
		t.Fatal("expected old file to be pruned by retention")
	}

	logStr := buf.String()
	if !strings.Contains(logStr, "run_id=") {
		t.Fatal("expected log to contain run_id")
	}
	if !strings.Contains(logStr, "start_time=") {
		t.Fatal("expected log to contain start_time")
	}
	if !strings.Contains(logStr, "end_time=") {
		t.Fatal("expected log to contain end_time")
	}
	if !strings.Contains(logStr, "byte_count=") {
		t.Fatal("expected log to contain byte_count")
	}

	re := regexp.MustCompile(`byte_count=(\d+)`)
	matches := re.FindAllStringSubmatch(logStr, -1)
	if len(matches) == 0 {
		t.Fatal("expected log to contain byte_count value")
	}
	lastMatch := matches[len(matches)-1][1]
	if lastMatch == "0" {
		t.Fatal("expected non-zero byte_count for successful backup")
	}
}

func TestBackupCmd_PgDumpFailure(t *testing.T) {
	restoreGlobals(t)
	t.Setenv("APP_PG_PASSWORD", "testpass")

	backupDir := t.TempDir()
	pqPath := filepath.Join(backupDir, "pq.pub")
	classicalPath := filepath.Join(backupDir, "classical.pub")

	_ = os.WriteFile(pqPath, []byte("pq"), 0o644)
	_ = os.WriteFile(classicalPath, []byte("classical"), 0o644)

	mockKM := &mockKeyManager{
		pub: &mockRecipientPub{
			schemeID: 0x0006,
			keyID:    make([]byte, 8),
			raw:      make([]byte, 32),
		},
	}
	newKeyManager = func(crypto.Registry) crypto.KeyManager {
		return mockKM
	}

	newRunner = func(...pipeline.Option) pipeline.Runner {
		return pipeline.NewRunner(
			pipeline.WithDumper(&failDumper{}),
			pipeline.WithEncryptor(&passthroughEncryptor{}),
		)
	}

	var buf bytes.Buffer
	outputWriter = &buf

	cmd := &cobra.Command{}
	setBackupFlags(cmd, backupDir, pqPath, classicalPath)

	err := backupCmd.RunE(cmd, []string{})
	if err == nil {
		t.Fatal("expected non-nil error")
	}

	entries, err := os.ReadDir(backupDir)
	if err != nil {
		t.Fatal(err)
	}
	for _, entry := range entries {
		name := entry.Name()
		if strings.HasSuffix(name, ".pqenc") || strings.HasSuffix(name, ".tmp") {
			t.Fatalf("unexpected file after failure: %s", name)
		}
	}

	logStr := buf.String()
	if !strings.Contains(logStr, "backup failed") {
		t.Fatal("expected 'backup failed' in log")
	}
	if !strings.Contains(logStr, "run_id=") {
		t.Fatal("expected log to contain run_id")
	}
	if !strings.Contains(logStr, "start_time=") {
		t.Fatal("expected log to contain start_time")
	}
	if !strings.Contains(logStr, "end_time=") {
		t.Fatal("expected log to contain end_time")
	}
	if !strings.Contains(logStr, "byte_count=0") {
		t.Fatal("expected log to contain byte_count=0 for failed backup")
	}
}
