package pgdump

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"reflect"
	"strings"
	"testing"
	"time"

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

// mockCommandContext creates a test helper that asserts the command name and args,
// and returns a shell command that produces the given stdout, stderr, and exit code.
func mockCommandContext(
	t *testing.T,
	wantName string,
	wantArgs []string,
	stdout string,
	stderr string,
	exitCode int,
) func(ctx context.Context, name string, arg ...string) *exec.Cmd {
	return func(ctx context.Context, name string, arg ...string) *exec.Cmd {
		if name != wantName {
			t.Errorf("command name = %q, want %q", name, wantName)
		}
		if !reflect.DeepEqual(arg, wantArgs) {
			t.Errorf("args = %v, want %v", arg, wantArgs)
		}
		script := fmt.Sprintf(
			"printf '%%s' '%s'; printf '%%s' '%s' >&2; exit %d",
			stdout,
			stderr,
			exitCode,
		)
		return exec.CommandContext(ctx, "sh", "-c", script)
	}
}

func TestDump_Success(t *testing.T) {
	wantArgs := make([]string, 0, 2)
	wantArgs = append(wantArgs, "--format=custom", "--exclude-table=e2e_one_time_keys_json")

	adapter := &adapter{
		commandContext: mockCommandContext(
			t,
			"pg_dump",
			wantArgs,
			"dumpdata",
			"",
			0,
		),
	}

	var buf bytes.Buffer
	err := adapter.Dump(
		context.Background(),
		pgdump.Options{Database: "testdb"},
		&buf,
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if buf.String() != "dumpdata" {
		t.Errorf("output = %q, want %q", buf.String(), "dumpdata")
	}
}

func TestDump_WaitAfterStdoutEOF(t *testing.T) {
	// This test verifies that after io.Copy returns (stdout EOF),
	// cmd.Wait() is called and the exit code is verified before returning.
	wantArgs := make([]string, 0, 2)
	wantArgs = append(wantArgs, "--format=custom", "--exclude-table=e2e_one_time_keys_json")

	adapter := &adapter{
		commandContext: mockCommandContext(
			t,
			"pg_dump",
			wantArgs,
			"dumpdata",
			"",
			0,
		),
	}

	var buf bytes.Buffer
	err := adapter.Dump(
		context.Background(),
		pgdump.Options{Database: "testdb"},
		&buf,
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if buf.String() != "dumpdata" {
		t.Errorf("output = %q, want %q", buf.String(), "dumpdata")
	}
}

func TestDump_NonZeroExitCode(t *testing.T) {
	wantArgs := make([]string, 0, 2)
	wantArgs = append(wantArgs, "--format=custom", "--exclude-table=e2e_one_time_keys_json")

	adapter := &adapter{
		commandContext: mockCommandContext(
			t,
			"pg_dump",
			wantArgs,
			"",
			"stderr error message",
			1,
		),
	}

	var buf bytes.Buffer
	err := adapter.Dump(
		context.Background(),
		pgdump.Options{Database: "testdb"},
		&buf,
	)
	if err == nil {
		t.Fatal("expected error, got nil")
	}

	if !errors.Is(err, pgdump.ErrPgDumpFailed(1)) {
		t.Errorf("error = %v, want ErrPgDumpFailed(1)", err)
	}
}

func TestDump_StderrCaptured(t *testing.T) {
	wantStderr := "stderr captured"
	var capturedCmd *exec.Cmd

	adapter := &adapter{
		commandContext: func(ctx context.Context, name string, arg ...string) *exec.Cmd {
			script := fmt.Sprintf("printf '%%s' '%s' >&2; exit 0", wantStderr)
			capturedCmd = exec.CommandContext(ctx, "sh", "-c", script)
			return capturedCmd
		},
	}

	var buf bytes.Buffer
	err := adapter.Dump(
		context.Background(),
		pgdump.Options{Database: "testdb"},
		&buf,
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if capturedCmd.Stderr == nil {
		t.Fatal("expected cmd.Stderr to be set, got nil")
	}

	stderrBuilder, ok := capturedCmd.Stderr.(*strings.Builder)
	if !ok {
		t.Fatalf("expected cmd.Stderr to be *strings.Builder, got %T", capturedCmd.Stderr)
	}

	if stderrBuilder.String() != wantStderr {
		t.Errorf("stderr = %q, want %q", stderrBuilder.String(), wantStderr)
	}
}

func TestDump_ContextCancellation(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
	defer cancel()

	adapter := &adapter{
		commandContext: func(ctx context.Context, name string, arg ...string) *exec.Cmd {
			return exec.CommandContext(ctx, "sh", "-c", "while :; do :; done")
		},
	}

	var buf bytes.Buffer
	err := adapter.Dump(ctx, pgdump.Options{Database: "testdb"}, &buf)
	if err == nil {
		t.Fatal("expected error due to context cancellation, got nil")
	}

	// Accept either context deadline exceeded or signal killed.
	if !errors.Is(err, context.DeadlineExceeded) && !strings.Contains(err.Error(), "signal") {
		t.Logf("got error: %v (acceptable variants: context.DeadlineExceeded or signal killed)", err)
	}
}

func TestDump_DefaultExcludeTables(t *testing.T) {
	wantArgs := make([]string, 0, 2)
	wantArgs = append(wantArgs, "--format=custom", "--exclude-table=e2e_one_time_keys_json")

	adapter := &adapter{
		commandContext: mockCommandContext(
			t,
			"pg_dump",
			wantArgs,
			"",
			"",
			0,
		),
	}

	err := adapter.Dump(
		context.Background(),
		pgdump.Options{Database: "testdb"},
		io.Discard,
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestDump_CustomExcludeTables(t *testing.T) {
	wantArgs := make([]string, 0, 3)
	wantArgs = append(wantArgs, "--format=custom", "--exclude-table=table_a", "--exclude-table=table_b")

	adapter := &adapter{
		commandContext: mockCommandContext(
			t,
			"pg_dump",
			wantArgs,
			"",
			"",
			0,
		),
	}

	err := adapter.Dump(
		context.Background(),
		pgdump.Options{
			Database:      "testdb",
			ExcludeTables: []string{"table_a", "table_b"},
		},
		io.Discard,
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestDump_EnvVars(t *testing.T) {
	var capturedCmd *exec.Cmd

	adapter := &adapter{
		commandContext: func(ctx context.Context, name string, arg ...string) *exec.Cmd {
			capturedCmd = exec.CommandContext(ctx, "sh", "-c", "exit 0")
			return capturedCmd
		},
	}

	err := adapter.Dump(
		context.Background(),
		pgdump.Options{
			Host:     "myhost",
			Port:     5433,
			User:     "myuser",
			Password: "mypass",
			Database: "mydb",
		},
		io.Discard,
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	envStr := strings.Join(capturedCmd.Env, "\n")

	if strings.Contains(envStr, "PGPASSWORD=") {
		t.Errorf("PGPASSWORD must not be passed to pg_dump subprocess")
	}

	wantEnvVars := make([]string, 0, 4)

	wantEnvVars = append(wantEnvVars, "PGHOST=myhost", "PGPORT=5433", "PGUSER=myuser", "PGDATABASE=mydb")

	for _, wantEnv := range wantEnvVars {
		if !strings.Contains(envStr, wantEnv) {
			t.Errorf("env missing %q", wantEnv)
		}
	}

	if !strings.Contains(envStr, "PGPASSFILE=") {
		t.Errorf("PGPASSFILE must be set when a password is provided")
	}
}

func TestDump_PgpassFileRemovedAfterRun(t *testing.T) {
	var capturedCmd *exec.Cmd

	adapter := &adapter{
		commandContext: func(ctx context.Context, name string, arg ...string) *exec.Cmd {
			capturedCmd = exec.CommandContext(ctx, "sh", "-c", "exit 0")
			return capturedCmd
		},
	}

	err := adapter.Dump(
		context.Background(),
		pgdump.Options{
			Password: "secret",
			Database: "mydb",
		},
		io.Discard,
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	envStr := strings.Join(capturedCmd.Env, "\n")
	passFilePrefix := "PGPASSFILE="
	idx := strings.Index(envStr, passFilePrefix)
	if idx == -1 {
		t.Fatalf("PGPASSFILE not found in env")
	}
	pgpassPath := envStr[idx+len(passFilePrefix):]
	if newlineIdx := strings.Index(pgpassPath, "\n"); newlineIdx != -1 {
		pgpassPath = pgpassPath[:newlineIdx]
	}

	if _, statErr := os.Stat(pgpassPath); !os.IsNotExist(statErr) {
		t.Errorf("temporary pgpass file %s was not removed after Dump returned", pgpassPath)
	}
}

func TestWritePgPassFile_Permissions(t *testing.T) {
	path, cleanup, err := writePgPassFile(pgdump.Options{
		Host:     "myhost",
		Port:     5432,
		Database: "mydb",
		User:     "myuser",
		Password: "secret",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	defer cleanup()

	info, err := os.Stat(path)
	if err != nil {
		t.Fatalf("stat pgpass file: %v", err)
	}
	if info.Mode().Perm() != 0o600 {
		t.Errorf("pgpass file permissions = %o, want %o", info.Mode().Perm(), 0o600)
	}

	content, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read pgpass file: %v", err)
	}
	want := "myhost:5432:mydb:myuser:secret\n"
	if string(content) != want {
		t.Errorf("pgpass content = %q, want %q", string(content), want)
	}
}

func TestDump_InvalidExcludeTable(t *testing.T) {
	adapter := &adapter{
		commandContext: func(ctx context.Context, name string, arg ...string) *exec.Cmd {
			return exec.CommandContext(ctx, "sh", "-c", "exit 0")
		},
	}

	err := adapter.Dump(
		context.Background(),
		pgdump.Options{
			Database:      "testdb",
			ExcludeTables: []string{"table; DROP TABLE users;--"},
		},
		io.Discard,
	)
	if err == nil {
		t.Fatal("expected error for invalid exclude-table identifier")
	}
	if !strings.Contains(err.Error(), "invalid exclude-table identifier") {
		t.Errorf("error = %v, want invalid exclude-table identifier", err)
	}
}

func TestDump_PipeWriterClosed(t *testing.T) {
	adapter := &adapter{
		commandContext: mockCommandContext(
			t,
			"pg_dump",
			[]string{
				"--format=custom",
				"--exclude-table=e2e_one_time_keys_json",
			},
			"pipe data",
			"",
			0,
		),
	}

	pr, pw := io.Pipe()
	readDone := make(chan struct{})
	var readData []byte
	var readErr error

	go func() {
		readData, readErr = io.ReadAll(pr)
		close(readDone)
	}()

	err := adapter.Dump(
		context.Background(),
		pgdump.Options{Database: "testdb"},
		pw,
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	<-readDone
	if readErr != nil {
		t.Fatalf("read error: %v", readErr)
	}
	if string(readData) != "pipe data" {
		t.Errorf("read data = %q, want %q", string(readData), "pipe data")
	}
}

func TestDump_PipeWriterClosedWithError(t *testing.T) {
	adapter := &adapter{
		commandContext: func(ctx context.Context, name string, arg ...string) *exec.Cmd {
			return exec.CommandContext(ctx, "sh", "-c", "exit 1")
		},
	}

	pr, pw := io.Pipe()
	readDone := make(chan struct{})
	var readErr error

	go func() {
		_, readErr = io.ReadAll(pr)
		close(readDone)
	}()

	err := adapter.Dump(
		context.Background(),
		pgdump.Options{Database: "testdb"},
		pw,
	)
	if err == nil {
		t.Fatal("expected error, got nil")
	}

	<-readDone
	if readErr == nil {
		t.Fatal("expected read error due to pipe close with error, got nil")
	}
	if !errors.Is(readErr, pgdump.ErrPgDumpFailed(1)) {
		t.Errorf("read error = %v, want ErrPgDumpFailed(1)", readErr)
	}
}
