package activitypub

import (
	"fmt"
	"reflect"
	"testing"
	"time"
)

func assertObjectWithTesting(fn canErrorFunc, expected Item) WithObjectFn {
	return func(p *Object) error {
		if !assertDeepEquals(fn, p, expected) {
			return fmt.Errorf("not equal")
		}
		return nil
	}
}

func TestOnObject(t *testing.T) {
	testObject := Object{
		ID: "https://example.com",
	}
	type args struct {
		it Item
		fn func(canErrorFunc, Item) WithObjectFn
	}
	tests := []struct {
		name     string
		args     args
		expected Item
		wantErr  bool
	}{
		{
			name:     "single",
			args:     args{testObject, assertObjectWithTesting},
			expected: &testObject,
			wantErr:  false,
		},
		{
			name:     "single fails",
			args:     args{Object{ID: "https://not-equals"}, assertObjectWithTesting},
			expected: &testObject,
			wantErr:  true,
		},
		{
			name:     "collectionOfObjects",
			args:     args{ItemCollection{testObject, testObject}, assertObjectWithTesting},
			expected: &testObject,
			wantErr:  false,
		},
		{
			name:     "collectionOfObjects fails",
			args:     args{ItemCollection{testObject, Object{ID: "https://not-equals"}}, assertObjectWithTesting},
			expected: &testObject,
			wantErr:  true,
		},
	}
	for _, tt := range tests {
		var logFn canErrorFunc
		if tt.wantErr {
			logFn = t.Logf
		} else {
			logFn = t.Errorf
		}
		t.Run(tt.name, func(t *testing.T) {
			if err := OnObject(tt.args.it, tt.args.fn(logFn, tt.expected)); (err != nil) != tt.wantErr {
				t.Errorf("OnObject() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func assertActivityWithTesting(fn canErrorFunc, expected Item) WithActivityFn {
	return func(p *Activity) error {
		if !assertDeepEquals(fn, p, expected) {
			return fmt.Errorf("not equal")
		}
		return nil
	}
}

func TestOnActivity(t *testing.T) {
	testActivity := Activity{
		ID: "https://example.com",
	}
	type args struct {
		it Item
		fn func(canErrorFunc, Item) WithActivityFn
	}
	tests := []struct {
		name     string
		args     args
		expected Item
		wantErr  bool
	}{
		{
			name:     "single",
			args:     args{testActivity, assertActivityWithTesting},
			expected: &testActivity,
			wantErr:  false,
		},
		{
			name:     "single fails",
			args:     args{Activity{ID: "https://not-equals"}, assertActivityWithTesting},
			expected: &testActivity,
			wantErr:  true,
		},
		{
			name:     "collectionOfActivitys",
			args:     args{ItemCollection{testActivity, testActivity}, assertActivityWithTesting},
			expected: &testActivity,
			wantErr:  false,
		},
		{
			name:     "collectionOfActivitys fails",
			args:     args{ItemCollection{testActivity, Activity{ID: "https://not-equals"}}, assertActivityWithTesting},
			expected: &testActivity,
			wantErr:  true,
		},
	}
	for _, tt := range tests {
		var logFn canErrorFunc
		if tt.wantErr {
			logFn = t.Logf
		} else {
			logFn = t.Errorf
		}
		t.Run(tt.name, func(t *testing.T) {
			if err := OnActivity(tt.args.it, tt.args.fn(logFn, tt.expected)); (err != nil) != tt.wantErr {
				t.Errorf("OnActivity() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func assertIntransitiveActivityWithTesting(fn canErrorFunc, expected Item) WithIntransitiveActivityFn {
	return func(p *IntransitiveActivity) error {
		if !assertDeepEquals(fn, p, expected) {
			return fmt.Errorf("not equal")
		}
		return nil
	}
}

func TestOnIntransitiveActivity(t *testing.T) {
	testIntransitiveActivity := IntransitiveActivity{
		ID: "https://example.com",
	}
	type args struct {
		it Item
		fn func(canErrorFunc, Item) WithIntransitiveActivityFn
	}
	tests := []struct {
		name     string
		args     args
		expected Item
		wantErr  bool
	}{
		{
			name:     "single",
			args:     args{testIntransitiveActivity, assertIntransitiveActivityWithTesting},
			expected: &testIntransitiveActivity,
			wantErr:  false,
		},
		{
			name:     "single fails",
			args:     args{IntransitiveActivity{ID: "https://not-equals"}, assertIntransitiveActivityWithTesting},
			expected: &testIntransitiveActivity,
			wantErr:  true,
		},
		{
			name:     "collectionOfIntransitiveActivitys",
			args:     args{ItemCollection{testIntransitiveActivity, testIntransitiveActivity}, assertIntransitiveActivityWithTesting},
			expected: &testIntransitiveActivity,
			wantErr:  false,
		},
		{
			name:     "collectionOfIntransitiveActivitys fails",
			args:     args{ItemCollection{testIntransitiveActivity, IntransitiveActivity{ID: "https://not-equals"}}, assertIntransitiveActivityWithTesting},
			expected: &testIntransitiveActivity,
			wantErr:  true,
		},
	}
	for _, tt := range tests {
		var logFn canErrorFunc
		if tt.wantErr {
			logFn = t.Logf
		} else {
			logFn = t.Errorf
		}
		t.Run(tt.name, func(t *testing.T) {
			if err := OnIntransitiveActivity(tt.args.it, tt.args.fn(logFn, tt.expected)); (err != nil) != tt.wantErr {
				t.Errorf("OnIntransitiveActivity() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func assertQuestionWithTesting(fn canErrorFunc, expected Item) WithQuestionFn {
	return func(p *Question) error {
		if !assertDeepEquals(fn, p, expected) {
			return fmt.Errorf("not equal")
		}
		return nil
	}
}

func TestOnQuestion(t *testing.T) {
	testQuestion := Question{
		ID: "https://example.com",
	}
	type args struct {
		it Item
		fn func(canErrorFunc, Item) WithQuestionFn
	}
	tests := []struct {
		name     string
		args     args
		expected Item
		wantErr  bool
	}{
		{
			name:     "single",
			args:     args{testQuestion, assertQuestionWithTesting},
			expected: &testQuestion,
			wantErr:  false,
		},
		{
			name:     "single fails",
			args:     args{Question{ID: "https://not-equals"}, assertQuestionWithTesting},
			expected: &testQuestion,
			wantErr:  true,
		},
		{
			name:     "collectionOfQuestions",
			args:     args{ItemCollection{testQuestion, testQuestion}, assertQuestionWithTesting},
			expected: &testQuestion,
			wantErr:  false,
		},
		{
			name:     "collectionOfQuestions fails",
			args:     args{ItemCollection{testQuestion, Question{ID: "https://not-equals"}}, assertQuestionWithTesting},
			expected: &testQuestion,
			wantErr:  true,
		},
	}
	for _, tt := range tests {
		var logFn canErrorFunc
		if tt.wantErr {
			logFn = t.Logf
		} else {
			logFn = t.Errorf
		}
		t.Run(tt.name, func(t *testing.T) {
			if err := OnQuestion(tt.args.it, tt.args.fn(logFn, tt.expected)); (err != nil) != tt.wantErr {
				t.Errorf("OnQuestion() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestOnCollection(t *testing.T) {
	t.Skipf("TODO")
}

func TestOnCollectionPage(t *testing.T) {
	t.Skipf("TODO")
}

func TestOnOrderedCollectionPage(t *testing.T) {
	t.Skipf("TODO")
}

type args[T Objects] struct {
	it T
	fn func(fn canErrorFunc, expected T) func(*T) error
}

type testPair[T Objects] struct {
	name     string
	args     args[T]
	expected T
	wantErr  bool
}

func assert[T Objects](fn canErrorFunc, expected T) func(*T) error {
	return func(p *T) error {
		if !assertDeepEquals(fn, *p, expected) {
			return fmt.Errorf("not equal")
		}
		return nil
	}
}

func TestOn(t *testing.T) {
	var tests = []testPair[Object]{
		{
			name:     "single object",
			args:     args[Object]{Object{ID: "https://example.com"}, assert[Object]},
			expected: Object{ID: "https://example.com"},
			wantErr:  false,
		},
		{
			name:     "single image",
			args:     args[Image]{Image{ID: "http://example.com"}, assert[Image]},
			expected: Image{ID: "http://example.com"},
			wantErr:  false,
		},
	}
	for _, tt := range tests {
		var logFn canErrorFunc
		if tt.wantErr {
			logFn = t.Logf
		} else {
			logFn = t.Errorf
		}
		t.Run(tt.name, func(t *testing.T) {
			if err := On(tt.args.it, tt.args.fn(logFn, tt.expected)); (err != nil) != tt.wantErr {
				t.Errorf("On[%T]() error = %v, wantErr %v", tt.args.it, err, tt.wantErr)
			}
		})
	}
}

var (
	emptyPrintFn = func(string, ...any) {}

	fnPrintObj = func(printFn func(string, ...any)) func(_ *Object) error {
		return func(o *Object) error {
			printFn("%v", o)
			return nil
		}
	}

	fnObj = func(_ *Object) error { return nil }
	fnAct = func(_ *Actor) error { return nil }
	fnA   = func(_ *Activity) error { return nil }
	fnIA  = func(_ *IntransitiveActivity) error { return nil }

	maybeObject               Item = new(Object)
	notObject                 Item = new(Activity)
	maybeActor                Item = new(Actor)
	maybeActivity             Item = new(Activity)
	notIntransitiveActivity   Item = new(Activity)
	maybeIntransitiveActivity Item = new(IntransitiveActivity)
	colOfObjects              Item = ItemCollection{Object{ID: "unum"}, Object{ID: "duo"}}
	colOfNotObjects           Item = ItemCollection{Activity{ID: "unum"}, Activity{ID: "duo"}}
)

func Benchmark_ToObject(b *testing.B) {
	for i := 0; i < b.N; i++ {
		ToObject(maybeObject)
	}
}

func Benchmark_To_T_Object(b *testing.B) {
	for i := 0; i < b.N; i++ {
		To[Object](maybeObject)
	}
}

func Benchmark_ToActor(b *testing.B) {
	for i := 0; i < b.N; i++ {
		ToActor(maybeActor)
	}
}

func Benchmark_To_T_Actor(b *testing.B) {
	for i := 0; i < b.N; i++ {
		To[Actor](maybeActor)
	}
}

func Benchmark_ToActivity(b *testing.B) {
	for i := 0; i < b.N; i++ {
		ToActivity(maybeActivity)
	}
}

func Benchmark_To_T_Activity(b *testing.B) {
	for i := 0; i < b.N; i++ {
		To[Activity](maybeActivity)
	}
}

func Benchmark_ToIntransitiveActivityHappy(b *testing.B) {
	for i := 0; i < b.N; i++ {
		ToIntransitiveActivity(maybeIntransitiveActivity)
	}
}

func Benchmark_To_T_IntransitiveActivityHappy(b *testing.B) {
	for i := 0; i < b.N; i++ {
		To[IntransitiveActivity](maybeIntransitiveActivity)
	}
}

func Benchmark_ToIntransitiveActivityNotHappy(b *testing.B) {
	for i := 0; i < b.N; i++ {
		ToIntransitiveActivity(notIntransitiveActivity)
	}
}

func Benchmark_To_T_IntransitiveActivityNotHappy(b *testing.B) {
	for i := 0; i < b.N; i++ {
		To[IntransitiveActivity](notIntransitiveActivity)
	}
}

func Benchmark_OnObject(b *testing.B) {
	for i := 0; i < b.N; i++ {
		OnObject(maybeObject, fnObj)
	}
}

func Benchmark_On_T_Object(b *testing.B) {
	for i := 0; i < b.N; i++ {
		On[Object](maybeObject, fnObj)
	}
}
func Benchmark_OnActor(b *testing.B) {
	for i := 0; i < b.N; i++ {
		OnActor(maybeObject, fnAct)
	}
}
func Benchmark_On_T_Actor(b *testing.B) {
	for i := 0; i < b.N; i++ {
		On[Actor](maybeObject, fnAct)
	}
}
func Benchmark_OnActivity(b *testing.B) {
	for i := 0; i < b.N; i++ {
		OnActivity(maybeObject, fnA)
	}
}

func Benchmark_On_T_Activity(b *testing.B) {
	for i := 0; i < b.N; i++ {
		On[Activity](maybeObject, fnA)
	}
}
func Benchmark_OnIntransitiveActivity(b *testing.B) {
	for i := 0; i < b.N; i++ {
		OnIntransitiveActivity(maybeObject, fnIA)
	}
}

func Benchmark_On_T_IntransitiveActivity(b *testing.B) {
	for i := 0; i < b.N; i++ {
		On[IntransitiveActivity](maybeObject, fnIA)
	}
}
func Benchmark_OnObjectNotHappy(b *testing.B) {
	for i := 0; i < b.N; i++ {
		OnObject(notObject, fnObj)
	}
}

func Benchmark_On_T_ObjectNotHappy(b *testing.B) {
	for i := 0; i < b.N; i++ {
		On[Object](notObject, fnObj)
	}
}

func Benchmark_OnObjectHappyCol(b *testing.B) {
	for i := 0; i < b.N; i++ {
		OnObject(colOfObjects, fnObj)
	}
}

func Benchmark_On_T_ObjectHappyCol(b *testing.B) {
	for i := 0; i < b.N; i++ {
		On[Object](colOfObjects, fnObj)
	}
}

func Benchmark_OnObjectNotHappyCol(b *testing.B) {
	for i := 0; i < b.N; i++ {
		OnObject(colOfNotObjects, fnObj)
	}
}

func Benchmark_On_T_ObjectNotHappyCol(b *testing.B) {
	for i := 0; i < b.N; i++ {
		On[Object](colOfNotObjects, fnObj)
	}
}

func TestDerefItem(t *testing.T) {
	tests := []struct {
		name string
		arg  Item
		want ItemCollection
	}{
		{
			name: "empty",
		},
		{
			name: "simple object",
			arg:  &Object{ID: "https://example.com"},
			want: ItemCollection{&Object{ID: "https://example.com"}},
		},
		{
			name: "simple IRI",
			arg:  IRI("https://example.com"),
			want: ItemCollection{IRI("https://example.com")},
		},
		{
			name: "IRI collection",
			arg:  IRIs{IRI("https://example.com"), IRI("https://example.com/~jdoe")},
			want: ItemCollection{IRI("https://example.com"), IRI("https://example.com/~jdoe")},
		},
		{
			name: "Item collection",
			arg: ItemCollection{
				&Object{ID: "https://example.com"},
				&Actor{ID: "https://example.com/~jdoe"},
			},
			want: ItemCollection{
				&Object{ID: "https://example.com"},
				&Actor{ID: "https://example.com/~jdoe"},
			},
		},
		{
			name: "mixed item collection",
			arg: ItemCollection{
				&Object{ID: "https://example.com"},
				IRI("https://example.com/666"),
				&Actor{ID: "https://example.com/~jdoe"},
			},
			want: ItemCollection{
				&Object{ID: "https://example.com"},
				IRI("https://example.com/666"),
				&Actor{ID: "https://example.com/~jdoe"},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := DerefItem(tt.arg); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("DerefItem() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestItemOrderTimestamp(t *testing.T) {
	early1 := time.Date(2001, 6, 6, 6, 0, 0, 0, time.UTC)
	early2 := time.Date(2001, 6, 6, 6, 0, 1, 0, time.UTC)
	late1 := time.Date(2001, 6, 6, 7, 0, 0, 0, time.UTC)
	late2 := time.Date(2001, 6, 6, 7, 0, 1, 0, time.UTC)
	type args struct {
		i1 Item
		i2 Item
	}
	tests := []struct {
		name string
		args args
		want bool
	}{
		{
			name: "empty",
			args: args{},
			want: false,
		},
		{
			name: "first empty",
			args: args{
				i1: nil,
				i2: &Object{},
			},
			want: true,
		},
		{
			name: "second empty",
			args: args{
				i1: &Object{},
				i2: nil,
			},
			want: false,
		},
		{
			name: "empty published/updated",
			args: args{
				i1: &Object{},
				i2: &Object{},
			},
			want: false,
		},
		{
			name: "first has empty published/updated",
			args: args{
				i1: &Object{},
				i2: &Object{Published: early1},
			},
			want: false,
		},
		{
			name: "check published equals",
			args: args{
				i1: &Object{Published: early1},
				i2: &Object{Published: early1},
			},
			want: false,
		},
		{
			name: "check published/updated equals",
			args: args{
				i1: &Object{Published: early2},
				i2: &Object{Updated: early2},
			},
			want: false,
		},
		{
			name: "check updated/published equals",
			args: args{
				i1: &Object{Updated: late1},
				i2: &Object{Published: late1},
			},
			want: false,
		},
		{
			name: "check first published earlier",
			args: args{
				i1: &Object{Published: early1},
				i2: &Object{Published: late1},
			},
			want: false,
		},
		{
			name: "check second published earlier",
			args: args{
				i1: &Object{Published: late1},
				i2: &Object{Published: early1},
			},
			want: true,
		},
		{
			name: "check first updated earlier",
			args: args{
				i1: &Object{Updated: early1},
				i2: &Object{Updated: late1},
			},
			want: false,
		},
		{
			name: "check second updated earlier",
			args: args{
				i1: &Object{Updated: late1},
				i2: &Object{Updated: early1},
			},
			want: true,
		},

		{
			name: "check first earlier",
			args: args{
				i1: &Object{Published: early1, Updated: late1},
				i2: &Object{Published: early1, Updated: late2},
			},
			want: false,
		},
		{
			name: "check second earlier",
			args: args{
				i1: &Object{Published: early1, Updated: late2},
				i2: &Object{Published: early1, Updated: late1},
			},
			want: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := ItemOrderTimestamp(tt.args.i1, tt.args.i2); got != tt.want {
				t.Errorf("ItemOrderTimestamp() = %v, want %v", got, tt.want)
			}
		})
	}
}
