package attachment

import (
	"context"
	"errors"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/objectstorage"
)

type uploadCleanupDBStub struct {
	tx *uploadCleanupTxStub
}

func (db *uploadCleanupDBStub) BeginTx(context.Context, coredb.TxOptions) (coredb.Tx, error) {
	return db.tx, nil
}

func (*uploadCleanupDBStub) QueryRow(context.Context, string, ...any) coredb.Row {
	panic("unexpected QueryRow on DB stub")
}

func (*uploadCleanupDBStub) Query(context.Context, string, ...any) (coredb.Rows, error) {
	panic("unexpected Query on DB stub")
}

func (*uploadCleanupDBStub) Exec(context.Context, string, ...any) (coredb.Result, error) {
	panic("unexpected Exec on DB stub")
}

type uploadCleanupTxStub struct {
	commitErr   error
	queryRows   coredb.Rows
	queryErr    error
	commits     int
	rollbacks   int
	execs       int
	execQueries []string
	execArgs    [][]any
}

func (*uploadCleanupTxStub) QueryRow(context.Context, string, ...any) coredb.Row {
	panic("unexpected QueryRow on Tx stub")
}

func (tx *uploadCleanupTxStub) Query(context.Context, string, ...any) (coredb.Rows, error) {
	if tx.queryErr != nil {
		return nil, tx.queryErr
	}
	if tx.queryRows != nil {
		return tx.queryRows, nil
	}
	panic("unexpected Query on Tx stub")
}

func (tx *uploadCleanupTxStub) Exec(_ context.Context, query string, args ...any) (coredb.Result, error) {
	tx.execs++
	tx.execQueries = append(tx.execQueries, query)
	tx.execArgs = append(tx.execArgs, append([]any(nil), args...))
	return uploadCleanupResultStub{rowsAffected: 1}, nil
}

func (tx *uploadCleanupTxStub) Commit(context.Context) error {
	tx.commits++
	return tx.commitErr
}

func (tx *uploadCleanupTxStub) Rollback(context.Context) error {
	tx.rollbacks++
	return nil
}

type uploadCleanupResultStub struct {
	rowsAffected int64
}

func (result uploadCleanupResultStub) RowsAffected() int64 {
	return result.rowsAffected
}

func TestUploadMethodsDeleteUploadedObjectsWhenCommitFails(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	tests := []struct {
		name       string
		options    Options
		call       func(*Store) error
		wantPrefix string
	}{
		{
			name: "UploadAttachmentsForUser",
			options: Options{
				ResolveUploadScopeForUser: func(context.Context, coredb.Queryer, string, string, string) (string, string, error) {
					return "mem-1", "spc-1", nil
				},
			},
			call: func(store *Store) error {
				_, err := store.UploadAttachmentsForUser(ctx, "usr-1", "wsp-1", "spc-1", []CreateAttachmentParams{{
					Filename:    "hello.png",
					ContentType: "image/png",
					Blob:        []byte("blob"),
				}})
				return err
			},
			wantPrefix: "attachments/wsp-1/",
		},
		{
			name: "UploadAttachmentsForMember",
			options: Options{
				ResolveUploadScopeForMember: func(context.Context, coredb.QueryRower, string, string, string) (string, error) {
					return "spc-1", nil
				},
			},
			call: func(store *Store) error {
				_, err := store.UploadAttachmentsForMember(ctx, "mem-1", "wsp-1", "spc-1", []CreateAttachmentParams{{
					Filename:    "hello.png",
					ContentType: "image/png",
					Blob:        []byte("blob"),
				}})
				return err
			},
			wantPrefix: "attachments/wsp-1/",
		},
		{
			name: "UploadPersonalStickersForUser",
			options: Options{
				LoadHumanMemberID: func(context.Context, coredb.QueryRower, string, string) (string, error) {
					return "mem-1", nil
				},
			},
			call: func(store *Store) error {
				_, err := store.UploadPersonalStickersForUser(ctx, "usr-1", "wsp-1", []CreateAttachmentParams{{
					Filename:    "sticker.png",
					ContentType: "image/png",
					Blob:        []byte("blob"),
				}})
				return err
			},
			wantPrefix: "stickers/wsp-1/",
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			fakeStorage := &fakeMediaStorage{driver: objectstorage.DriverOSS}
			tx := &uploadCleanupTxStub{commitErr: errors.New("boom")}
			store := &Store{
				db: &uploadCleanupDBStub{tx: tx},
				sharedStore: sharedStore{
					mediaObjectStorage: fakeStorage,
					options:            tt.options,
				},
			}

			err := tt.call(store)
			require.Error(t, err)
			require.ErrorContains(t, err, "commit tx")
			require.Len(t, fakeStorage.uploaded, 1)
			require.Equal(t, fakeStorage.uploaded, fakeStorage.deleted)
			require.True(t, strings.HasPrefix(fakeStorage.deleted[0], tt.wantPrefix), "deleted key %q", fakeStorage.deleted[0])
			require.Equal(t, 1, tx.execs)
			require.Equal(t, 1, tx.commits)
			require.Equal(t, 1, tx.rollbacks)
		})
	}
}

func TestUploadAttachmentsToScopeBatchesInserts(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	tx := &uploadCleanupTxStub{}
	txs := TxStore{tx: tx}

	created, err := txs.uploadAttachmentsToScope(ctx, " mem-1 ", "wsp-1", "spc-1", []CreateAttachmentParams{
		{Filename: "first.png", ContentType: "image/png", Blob: []byte("first")},
		{Filename: "second.txt", Blob: []byte("second")},
	})
	require.NoError(t, err)
	require.Len(t, created, 2)
	require.Equal(t, "first.png", created[0].Filename)
	require.Equal(t, "second.txt", created[1].Filename)
	require.Equal(t, "application/octet-stream", created[1].ContentType)
	require.Equal(t, 1, tx.execs)
	require.Len(t, tx.execQueries, 1)
	require.Contains(t, tx.execQueries[0], "message_id")
	require.Contains(t, tx.execQueries[0], "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12),")
	require.Contains(t, tx.execQueries[0], "($13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24)")
	require.Len(t, tx.execArgs[0], 24)
	require.Equal(t, "wsp-1", tx.execArgs[0][1])
	require.Equal(t, "spc-1", tx.execArgs[0][2])
	require.Equal(t, "mem-1", tx.execArgs[0][3])
	require.Nil(t, tx.execArgs[0][4])
	require.Equal(t, "first.png", tx.execArgs[0][5])
	require.Equal(t, int64(5), tx.execArgs[0][7])
	require.Equal(t, []byte("first"), tx.execArgs[0][10])
	require.Equal(t, "wsp-1", tx.execArgs[0][13])
	require.Equal(t, "spc-1", tx.execArgs[0][14])
	require.Equal(t, "mem-1", tx.execArgs[0][15])
	require.Nil(t, tx.execArgs[0][16])
	require.Equal(t, "second.txt", tx.execArgs[0][17])
	require.Equal(t, "application/octet-stream", tx.execArgs[0][18])
	require.Equal(t, int64(6), tx.execArgs[0][19])
	require.Equal(t, []byte("second"), tx.execArgs[0][22])
}

func TestUploadPersonalStickersForUserBatchesInserts(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	tx := &uploadCleanupTxStub{}
	txs := TxStore{
		tx: tx,
		sharedStore: sharedStore{
			options: Options{
				LoadHumanMemberID: func(context.Context, coredb.QueryRower, string, string) (string, error) {
					return "mem-1", nil
				},
			},
		},
	}

	created, err := txs.UploadPersonalStickersForUser(ctx, "usr-1", "wsp-1", []CreateAttachmentParams{
		{Filename: "first.png", ContentType: "image/png", Blob: []byte("first")},
		{Filename: "second.png", ContentType: "image/png", Blob: []byte("second")},
	})
	require.NoError(t, err)
	require.Len(t, created, 2)
	require.Equal(t, "first.png", created[0].Filename)
	require.Equal(t, "second.png", created[1].Filename)
	require.Equal(t, 1, tx.execs)
	require.Len(t, tx.execQueries, 1)
	require.Contains(t, tx.execQueries[0], "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10),")
	require.Contains(t, tx.execQueries[0], "($11, $12, $13, $14, $15, $16, $17, $18, $19, $20)")
	require.Len(t, tx.execArgs[0], 20)
	require.Equal(t, "wsp-1", tx.execArgs[0][1])
	require.Equal(t, "mem-1", tx.execArgs[0][2])
	require.Equal(t, "first.png", tx.execArgs[0][3])
	require.Equal(t, int64(5), tx.execArgs[0][5])
	require.Equal(t, []byte("first"), tx.execArgs[0][8])
	require.Equal(t, "wsp-1", tx.execArgs[0][11])
	require.Equal(t, "mem-1", tx.execArgs[0][12])
	require.Equal(t, "second.png", tx.execArgs[0][13])
	require.Equal(t, int64(6), tx.execArgs[0][15])
	require.Equal(t, []byte("second"), tx.execArgs[0][18])
}

func TestCopyMessageAttachmentsBatchesInserts(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	tx := &uploadCleanupTxStub{
		queryRows: &copyAttachmentRows{rows: []copyAttachmentRow{
			{
				messageOrdinality:  1,
				sourceMessageID:    "msg-source-1",
				sourceAttachmentID: "att-source-1",
				uploadedByMemberID: "mem-source",
				filename:           "first.png",
				contentType:        "image/png",
				sizeBytes:          5,
				storageDriver:      "oss",
				objectKey:          "attachments/wsp-1/first",
				payload:            nil,
				createdAt:          time.Date(2026, 6, 7, 19, 0, 0, 0, time.UTC),
			},
			{
				messageOrdinality:  2,
				sourceMessageID:    "msg-source-2",
				sourceAttachmentID: "att-source-2",
				uploadedByMemberID: "mem-source",
				filename:           "second.txt",
				contentType:        "text/plain",
				sizeBytes:          6,
				payload:            []byte("second"),
				createdAt:          time.Date(2026, 6, 7, 19, 1, 0, 0, time.UTC),
			},
		}},
	}
	txs := TxStore{tx: tx}

	created, err := txs.CopyMessageAttachments(ctx, "wsp-1", "spc-target", "mem-target", "msg-target", []string{"msg-source-1", "msg-source-2"})
	require.NoError(t, err)
	require.Len(t, created, 2)
	require.Equal(t, "first.png", created[0].Filename)
	require.Equal(t, "second.txt", created[1].Filename)
	require.Equal(t, 1, tx.execs)
	require.Len(t, tx.execQueries, 1)
	require.Contains(t, tx.execQueries[0], "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12),")
	require.Contains(t, tx.execQueries[0], "($13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24)")
	require.Len(t, tx.execArgs[0], 24)
	require.Equal(t, "wsp-1", tx.execArgs[0][1])
	require.Equal(t, "spc-target", tx.execArgs[0][2])
	require.Equal(t, "mem-target", tx.execArgs[0][3])
	require.Equal(t, "msg-target", tx.execArgs[0][4])
	require.Equal(t, "first.png", tx.execArgs[0][5])
	require.Equal(t, "oss", tx.execArgs[0][8])
	require.Equal(t, "attachments/wsp-1/first", tx.execArgs[0][9])
	require.Nil(t, tx.execArgs[0][10])
	require.Equal(t, "wsp-1", tx.execArgs[0][13])
	require.Equal(t, "spc-target", tx.execArgs[0][14])
	require.Equal(t, "mem-target", tx.execArgs[0][15])
	require.Equal(t, "msg-target", tx.execArgs[0][16])
	require.Equal(t, "second.txt", tx.execArgs[0][17])
	require.Nil(t, tx.execArgs[0][21])
	require.Equal(t, []byte("second"), tx.execArgs[0][22])
}

type copyAttachmentRow struct {
	messageOrdinality  int
	sourceMessageID    string
	sourceAttachmentID string
	uploadedByMemberID string
	filename           string
	contentType        string
	sizeBytes          int64
	storageDriver      string
	objectKey          string
	payload            []byte
	createdAt          time.Time
}

type copyAttachmentRows struct {
	rows []copyAttachmentRow
	idx  int
}

func (rows *copyAttachmentRows) Close() {}

func (rows *copyAttachmentRows) Err() error { return nil }

func (rows *copyAttachmentRows) Next() bool {
	if rows.idx >= len(rows.rows) {
		return false
	}
	rows.idx++
	return true
}

func (rows *copyAttachmentRows) Scan(dest ...any) error {
	row := rows.rows[rows.idx-1]
	*dest[0].(*int) = row.messageOrdinality
	*dest[1].(*string) = row.sourceMessageID
	*dest[2].(*string) = row.sourceAttachmentID
	*dest[3].(*string) = row.uploadedByMemberID
	*dest[4].(*string) = row.filename
	*dest[5].(*string) = row.contentType
	*dest[6].(*int64) = row.sizeBytes
	*dest[7].(*string) = row.storageDriver
	*dest[8].(*string) = row.objectKey
	*dest[9].(*[]byte) = row.payload
	*dest[10].(*time.Time) = row.createdAt
	return nil
}
