package mongo

import (
	"errors"
	"fmt"
	"testing"

	mongodriver "go.mongodb.org/mongo-driver/v2/mongo"
)

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

	tests := []struct {
		name string
		err  error
		want bool
	}{
		{
			name: "nil error",
			err:  nil,
			want: false,
		},
		{
			name: "unrelated error",
			err:  errors.New("connection refused"),
			want: false,
		},
		{
			name: "write exception duplicate key",
			err: writeExceptionWithMessage(
				11000,
				`E11000 duplicate key error collection: tm.users index: phone_idx dup key: { phone: "+1234567890" }`,
			),
			want: true,
		},
		{
			name: "write exception non duplicate code",
			err: writeExceptionWithMessage(
				121,
				`Document failed validation`,
			),
			want: false,
		},
		{
			name: "wrapped write exception duplicate key",
			err: fmt.Errorf(
				"failed to create user: %w",
				writeExceptionWithMessage(
					11000,
					`E11000 duplicate key error collection: tm.users index: username_idx dup key: { username: "admin" }`,
				),
			),
			want: true,
		},
		{
			name: "bulk write exception duplicate key",
			err: bulkWriteExceptionWithMessage(
				11000,
				`E11000 duplicate key error collection: tm.customers index: phone_idx dup key: { phone: "+1234567890" }`,
			),
			want: true,
		},
		{
			name: "string fallback duplicate key",
			err:  errors.New(`write exception: write errors: [E11000 duplicate key error collection: tm.users index: phone_idx dup key: { phone: "+1234567890" }] `),
			want: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			if got := IsDuplicateKeyError(tt.err); got != tt.want {
				t.Fatalf("IsDuplicateKeyError() = %v, want %v", got, tt.want)
			}
		})
	}
}

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

	const phoneMsg = `E11000 duplicate key error collection: tm.users index: phone_idx dup key: { phone: "+1234567890" }`
	const usernameMsg = `E11000 duplicate key error collection: tm.users index: username_idx dup key: { username: "admin" }`
	const emailMsg = `E11000 duplicate key error collection: tm.customers index: email_1 dup key: { email: "user@example.com" }`
	const phoneValueContainsNameMsg = `E11000 duplicate key error collection: tm.users index: phone_idx dup key: { phone: "username-like-value" }`

	tests := []struct {
		name  string
		err   error
		field string
		want  bool
	}{
		{
			name:  "nil error",
			err:   nil,
			field: "phone",
			want:  false,
		},
		{
			name:  "empty field",
			err:   writeExceptionWithMessage(11000, phoneMsg),
			field: "",
			want:  false,
		},
		{
			name:  "phone duplicate matches phone field",
			err:   writeExceptionWithMessage(11000, phoneMsg),
			field: "phone",
			want:  true,
		},
		{
			name:  "phone duplicate does not match email field",
			err:   writeExceptionWithMessage(11000, phoneMsg),
			field: "email",
			want:  false,
		},
		{
			name:  "username duplicate matches username field",
			err:   writeExceptionWithMessage(11000, usernameMsg),
			field: "username",
			want:  true,
		},
		{
			name:  "username duplicate does not match name substring",
			err:   writeExceptionWithMessage(11000, usernameMsg),
			field: "name",
			want:  false,
		},
		{
			name:  "email duplicate matches email field via default index suffix",
			err:   writeExceptionWithMessage(11000, emailMsg),
			field: "email",
			want:  true,
		},
		{
			name:  "dup key value substring does not match unrelated field",
			err:   writeExceptionWithMessage(11000, phoneValueContainsNameMsg),
			field: "name",
			want:  false,
		},
		{
			name:  "wrapped duplicate key matches field",
			err: fmt.Errorf(
				"failed to register customer: %w",
				writeExceptionWithMessage(11000, phoneMsg),
			),
			field: "phone",
			want:  true,
		},
		{
			name:  "non duplicate write error does not match",
			err:   writeExceptionWithMessage(121, phoneMsg),
			field: "phone",
			want:  false,
		},
		{
			name:  "unrelated error does not match",
			err:   errors.New("timeout"),
			field: "phone",
			want:  false,
		},
		{
			name:  "string fallback duplicate key matches field",
			err:   errors.New(`write exception: write errors: [E11000 duplicate key error collection: tm.users index: phone_idx dup key: { phone: "+1234567890" }] `),
			field: "phone",
			want:  true,
		},
		{
			name:  "string fallback duplicate key does not match unrelated field",
			err:   errors.New(`write exception: write errors: [E11000 duplicate key error collection: tm.users index: username_idx dup key: { username: "admin" }] `),
			field: "name",
			want:  false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			if got := DuplicateKeyMatchesField(tt.err, tt.field); got != tt.want {
				t.Fatalf("DuplicateKeyMatchesField(%q) = %v, want %v", tt.field, got, tt.want)
			}
		})
	}
}

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

	tests := []struct {
		name    string
		message string
		want    string
	}{
		{
			name:    "phone index",
			message: `E11000 duplicate key error collection: tm.users index: phone_idx dup key: { phone: "+1234567890" }`,
			want:    "phone_idx",
		},
		{
			name:    "missing index segment",
			message: `E11000 duplicate key error collection: tm.users dup key: { phone: "+1234567890" }`,
			want:    "",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			if got := extractDuplicateIndexName(tt.message); got != tt.want {
				t.Fatalf("extractDuplicateIndexName() = %q, want %q", got, tt.want)
			}
		})
	}
}

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

	tests := []struct {
		name    string
		message string
		want    []string
	}{
		{
			name:    "single field",
			message: `index: phone_idx dup key: { phone: "+1234567890" }`,
			want:    []string{"phone"},
		},
		{
			name:    "compound dup key",
			message: `index: user_company_idx dup key: { user_id: ObjectId('507f1f77bcf86cd799439011'), company_id: ObjectId('507f1f77bcf86cd799439012') }`,
			want:    []string{"user_id", "company_id"},
		},
		{
			name:    "value containing colon does not add false field",
			message: `index: phone_idx dup key: { phone: "office:123" }`,
			want:    []string{"phone"},
		},
		{
			name:    "missing dup key segment",
			message: `index: phone_idx`,
			want:    nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			got := extractDuplicateKeyFields(tt.message)
			if len(got) != len(tt.want) {
				t.Fatalf("extractDuplicateKeyFields() = %v, want %v", got, tt.want)
			}
			for i := range got {
				if got[i] != tt.want[i] {
					t.Fatalf("extractDuplicateKeyFields() = %v, want %v", got, tt.want)
				}
			}
		})
	}
}

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

	tests := []struct {
		indexName string
		field     string
		want      bool
	}{
		{indexName: "phone_idx", field: "phone", want: true},
		{indexName: "phone_1", field: "phone", want: true},
		{indexName: "phone", field: "phone", want: true},
		{indexName: "username_idx", field: "name", want: false},
		{indexName: "email_idx", field: "mail", want: false},
	}

	for _, tt := range tests {
		t.Run(tt.indexName+"_"+tt.field, func(t *testing.T) {
			t.Parallel()
			if got := indexNameMatchesField(tt.indexName, tt.field); got != tt.want {
				t.Fatalf("indexNameMatchesField(%q, %q) = %v, want %v", tt.indexName, tt.field, got, tt.want)
			}
		})
	}
}

func writeExceptionWithMessage(code int, message string) error {
	return mongodriver.WriteException{
		WriteErrors: []mongodriver.WriteError{
			{Code: code, Message: message},
		},
	}
}

func bulkWriteExceptionWithMessage(code int, message string) error {
	return mongodriver.BulkWriteException{
		WriteErrors: []mongodriver.BulkWriteError{
			{WriteError: mongodriver.WriteError{Code: code, Message: message}},
		},
	}
}
