package database

import (
	"context"
	"testing"
	"time"

	"github.com/go-shiori/shiori/internal/model"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

type databaseTestCase func(t *testing.T, db DB)
type testDatabaseFactory func(t *testing.T, ctx context.Context) (DB, error)

func testDatabase(t *testing.T, dbFactory testDatabaseFactory) {
	tests := map[string]databaseTestCase{
		// Bookmarks
		"testBookmarkAutoIncrement":             testBookmarkAutoIncrement,
		"testCreateBookmark":                    testCreateBookmark,
		"testCreateBookmarkWithContent":         testCreateBookmarkWithContent,
		"testCreateBookmarkTwice":               testCreateBookmarkTwice,
		"testCreateBookmarkWithTag":             testCreateBookmarkWithTag,
		"testCreateTwoDifferentBookmarks":       testCreateTwoDifferentBookmarks,
		"testUpdateBookmark":                    testUpdateBookmark,
		"testUpdateBookmarkUpdatesModifiedTime": testUpdateBookmarkUpdatesModifiedTime,
		"testGetBoomarksWithTimeFilters":        testGetBoomarksWithTimeFilters,
		"testUpdateBookmarkWithContent":         testUpdateBookmarkWithContent,
		"testGetBookmark":                       testGetBookmark,
		"testGetBookmarkNotExistent":            testGetBookmarkNotExistent,
		"testGetBookmarks":                      testGetBookmarks,
		"testGetBookmarksWithSQLCharacters":     testGetBookmarksWithSQLCharacters,
		"testGetBookmarksCount":                 testGetBookmarksCount,
		// Tags
		"testCreateTag":  testCreateTag,
		"testCreateTags": testCreateTags,
		// Accounts
		"testSaveAccount":        testSaveAccount,
		"testSaveAccountSetting": testSaveAccountSettings,
		"testGetAccount":         testGetAccount,
		"testGetAccounts":        testGetAccounts,
	}

	for testName, testCase := range tests {
		t.Run(testName, func(tInner *testing.T) {
			ctx := context.TODO()
			db, err := dbFactory(t, ctx)
			require.NoError(tInner, err, "Error recreating database")
			testCase(tInner, db)
		})
	}
}

func testBookmarkAutoIncrement(t *testing.T, db DB) {
	ctx := context.TODO()

	book := model.BookmarkDTO{
		URL:   "https://github.com/go-shiori/shiori",
		Title: "shiori",
	}

	result, err := db.SaveBookmarks(ctx, true, book)
	assert.NoError(t, err, "Save bookmarks must not fail")
	assert.Equal(t, 1, result[0].ID, "Saved bookmark must have ID %d", 1)

	book = model.BookmarkDTO{
		URL:   "https://github.com/go-shiori/obelisk",
		Title: "obelisk",
	}

	result, err = db.SaveBookmarks(ctx, true, book)
	assert.NoError(t, err, "Save bookmarks must not fail")
	assert.Equal(t, 2, result[0].ID, "Saved bookmark must have ID %d", 2)
}

func testCreateBookmark(t *testing.T, db DB) {
	ctx := context.TODO()

	book := model.BookmarkDTO{
		URL:   "https://github.com/go-shiori/obelisk",
		Title: "shiori",
	}

	result, err := db.SaveBookmarks(ctx, true, book)

	assert.NoError(t, err, "Save bookmarks must not fail")
	assert.Equal(t, 1, result[0].ID, "Saved bookmark must have an ID set")
}

func testCreateBookmarkWithContent(t *testing.T, db DB) {
	ctx := context.TODO()

	book := model.BookmarkDTO{
		URL:     "https://github.com/go-shiori/obelisk",
		Title:   "shiori",
		Content: "Some content",
		HTML:    "Some HTML content",
	}

	result, err := db.SaveBookmarks(ctx, true, book)
	assert.NoError(t, err, "Save bookmarks must not fail")

	books, err := db.GetBookmarks(ctx, GetBookmarksOptions{
		IDs:         []int{result[0].ID},
		WithContent: true,
	})
	assert.NoError(t, err, "Get bookmarks must not fail")
	assert.Len(t, books, 1)

	assert.Equal(t, 1, books[0].ID, "Saved bookmark must have an ID set")
	assert.Equal(t, book.Content, books[0].Content, "Saved bookmark must have content")
	assert.Equal(t, book.HTML, books[0].HTML, "Saved bookmark must have HTML")
}

func testCreateBookmarkWithTag(t *testing.T, db DB) {
	ctx := context.TODO()

	book := model.BookmarkDTO{
		URL:   "https://github.com/go-shiori/obelisk",
		Title: "shiori",
		Tags: []model.Tag{
			{
				Name: "test-tag",
			},
		},
	}

	result, err := db.SaveBookmarks(ctx, true, book)

	assert.NoError(t, err, "Save bookmarks must not fail")
	assert.Equal(t, book.URL, result[0].URL)
	assert.Equal(t, book.Tags[0].Name, result[0].Tags[0].Name)
}

func testCreateBookmarkTwice(t *testing.T, db DB) {
	ctx := context.TODO()

	book := model.BookmarkDTO{
		URL:   "https://github.com/go-shiori/shiori",
		Title: "shiori",
	}

	result, err := db.SaveBookmarks(ctx, true, book)
	assert.NoError(t, err, "Save bookmarks must not fail")

	savedBookmark := result[0]
	savedBookmark.Title = "modified"

	_, err = db.SaveBookmarks(ctx, true, savedBookmark)
	assert.Error(t, err, "Save bookmarks must fail")
}

func testCreateTwoDifferentBookmarks(t *testing.T, db DB) {
	ctx := context.TODO()

	book := model.BookmarkDTO{
		URL:   "https://github.com/go-shiori/shiori",
		Title: "shiori",
	}

	_, err := db.SaveBookmarks(ctx, true, book)
	assert.NoError(t, err, "Save first bookmark must not fail")

	book = model.BookmarkDTO{
		URL:   "https://github.com/go-shiori/go-readability",
		Title: "go-readability",
	}
	_, err = db.SaveBookmarks(ctx, true, book)
	assert.NoError(t, err, "Save second bookmark must not fail")
}

func testUpdateBookmark(t *testing.T, db DB) {
	ctx := context.TODO()

	book := model.BookmarkDTO{
		URL:   "https://github.com/go-shiori/shiori",
		Title: "shiori",
	}

	result, err := db.SaveBookmarks(ctx, true, book)
	assert.NoError(t, err, "Save bookmarks must not fail")

	savedBookmark := result[0]
	savedBookmark.Title = "modified"

	result, err = db.SaveBookmarks(ctx, false, savedBookmark)
	assert.NoError(t, err, "Save bookmarks must not fail")

	assert.Equal(t, "modified", result[0].Title)
	assert.Equal(t, savedBookmark.ID, result[0].ID)
}

func testUpdateBookmarkWithContent(t *testing.T, db DB) {
	ctx := context.TODO()

	book := model.BookmarkDTO{
		URL:     "https://github.com/go-shiori/obelisk",
		Title:   "shiori",
		Content: "Some content",
		HTML:    "Some HTML content",
	}

	result, err := db.SaveBookmarks(ctx, true, book)
	assert.NoError(t, err, "Save bookmarks must not fail")

	updatedBook := result[0]
	updatedBook.Content = "Some updated content"
	updatedBook.HTML = "Some updated HTML content"

	_, err = db.SaveBookmarks(ctx, false, updatedBook)
	assert.NoError(t, err, "Save bookmarks must not fail")

	books, err := db.GetBookmarks(ctx, GetBookmarksOptions{
		IDs:         []int{result[0].ID},
		WithContent: true,
	})
	assert.NoError(t, err, "Get bookmarks must not fail")
	assert.Len(t, books, 1)

	assert.Equal(t, 1, books[0].ID, "Saved bookmark must have an ID set")
	assert.Equal(t, updatedBook.Content, books[0].Content, "Saved bookmark must have updated content")
	assert.Equal(t, updatedBook.HTML, books[0].HTML, "Saved bookmark must have updated HTML")
}

func testGetBookmark(t *testing.T, db DB) {
	ctx := context.TODO()

	book := model.BookmarkDTO{
		URL:   "https://github.com/go-shiori/shiori",
		Title: "shiori",
	}

	result, err := db.SaveBookmarks(ctx, true, book)
	assert.NoError(t, err, "Save bookmarks must not fail")

	savedBookmark, exists, err := db.GetBookmark(ctx, result[0].ID, "")
	assert.True(t, exists, "Bookmark should exist")
	assert.NoError(t, err, "Get bookmark should not fail")
	assert.Equal(t, result[0].ID, savedBookmark.ID, "Retrieved bookmark should be the same")
	assert.Equal(t, book.URL, savedBookmark.URL, "Retrieved bookmark should be the same")
}

func testGetBookmarkNotExistent(t *testing.T, db DB) {
	ctx := context.TODO()

	savedBookmark, exists, err := db.GetBookmark(ctx, 1, "")
	assert.NoError(t, err, "Get bookmark should not fail")
	assert.False(t, exists, "Bookmark should not exist")
	assert.Equal(t, model.BookmarkDTO{}, savedBookmark)
}

func testGetBookmarks(t *testing.T, db DB) {
	ctx := context.TODO()

	book := model.BookmarkDTO{
		URL:   "https://github.com/go-shiori/shiori",
		Title: "shiori",
	}

	bookmarks, err := db.SaveBookmarks(ctx, true, book)
	assert.NoError(t, err, "Save bookmarks must not fail")

	savedBookmark := bookmarks[0]

	results, err := db.GetBookmarks(ctx, GetBookmarksOptions{
		Keyword: "go-shiori",
	})

	assert.NoError(t, err, "Get bookmarks should not fail")
	assert.Len(t, results, 1, "results should contain one item")
	assert.Equal(t, savedBookmark.ID, results[0].ID, "bookmark should be the one saved")
}

func testGetBookmarksWithSQLCharacters(t *testing.T, db DB) {
	ctx := context.TODO()

	// _ := 0
	book := model.BookmarkDTO{
		URL:   "https://github.com/go-shiori/shiori",
		Title: "shiori",
	}
	_, err := db.SaveBookmarks(ctx, true, book)
	assert.NoError(t, err, "Save bookmarks must not fail")

	characters := []string{";", "%", "_", "\\", "\"", ":"}

	for _, char := range characters {
		t.Run("GetBookmarks/"+char, func(t *testing.T) {
			_, err := db.GetBookmarks(ctx, GetBookmarksOptions{
				Keyword: char,
			})
			assert.NoError(t, err, "Get bookmarks should not fail")
		})

		t.Run("GetBookmarksCount/"+char, func(t *testing.T) {
			_, err := db.GetBookmarksCount(ctx, GetBookmarksOptions{
				Keyword: char,
			})
			assert.NoError(t, err, "Get bookmarks count should not fail")
		})
	}
}

func testGetBookmarksCount(t *testing.T, db DB) {
	ctx := context.TODO()

	expectedCount := 1
	book := model.BookmarkDTO{
		URL:   "https://github.com/go-shiori/shiori",
		Title: "shiori",
	}

	_, err := db.SaveBookmarks(ctx, true, book)
	assert.NoError(t, err, "Save bookmarks must not fail")

	count, err := db.GetBookmarksCount(ctx, GetBookmarksOptions{
		Keyword: "go-shiori",
	})
	assert.NoError(t, err, "Get bookmarks count should not fail")
	assert.Equal(t, count, expectedCount, "count should be %d", expectedCount)
}

func testCreateTag(t *testing.T, db DB) {
	ctx := context.TODO()
	tag := model.Tag{Name: "shiori"}
	err := db.CreateTags(ctx, tag)
	assert.NoError(t, err, "Save tag must not fail")
}

func testCreateTags(t *testing.T, db DB) {
	ctx := context.TODO()
	err := db.CreateTags(ctx, model.Tag{Name: "shiori"}, model.Tag{Name: "shiori2"})
	assert.NoError(t, err, "Save tag must not fail")
}

func testSaveAccount(t *testing.T, db DB) {
	ctx := context.TODO()

	t.Run("success", func(t *testing.T) {
		acc := model.Account{
			Username: "testuser",
			Config:   model.UserConfig{},
		}

		err := db.SaveAccount(ctx, acc)
		require.Nil(t, err)
	})
}

func testSaveAccountSettings(t *testing.T, db DB) {
	ctx := context.TODO()

	t.Run("success", func(t *testing.T) {
		acc := model.Account{
			Username: "test",
			Config:   model.UserConfig{},
		}

		err := db.SaveAccountSettings(ctx, acc)
		require.Nil(t, err)
	})
}

func testGetAccount(t *testing.T, db DB) {
	ctx := context.TODO()

	t.Run("success", func(t *testing.T) {
		// Insert test accounts
		testAccounts := []model.Account{
			{Username: "foo", Password: "bar", Owner: false},
			{Username: "hello", Password: "world", Owner: false},
			{Username: "foo_bar", Password: "foobar", Owner: true},
		}
		for _, acc := range testAccounts {
			err := db.SaveAccount(ctx, acc)
			assert.Nil(t, err)

			// Successful case
			account, exists, err := db.GetAccount(ctx, acc.Username)
			assert.Nil(t, err)
			assert.True(t, exists, "Expected account to exist")
			assert.Equal(t, acc.Username, account.Username)
		}
		// Falid case
		account, exists, err := db.GetAccount(ctx, "foobar")
		assert.NotNil(t, err)
		assert.False(t, exists, "Expected account to exist")
		assert.Empty(t, account.Username)
	})
}

func testGetAccounts(t *testing.T, db DB) {
	ctx := context.TODO()

	t.Run("success", func(t *testing.T) {
		// Insert test accounts
		testAccounts := []model.Account{
			{Username: "foo", Password: "bar", Owner: false},
			{Username: "hello", Password: "world", Owner: false},
			{Username: "foo_bar", Password: "foobar", Owner: true},
		}
		for _, acc := range testAccounts {
			err := db.SaveAccount(ctx, acc)
			assert.Nil(t, err)
		}

		// Successful case
		// without opt
		accounts, err := db.GetAccounts(ctx, GetAccountsOptions{})
		assert.NoError(t, err)
		assert.Equal(t, 3, len(accounts))
		// with owner
		accounts, err = db.GetAccounts(ctx, GetAccountsOptions{Owner: true})
		assert.NoError(t, err)
		assert.Equal(t, 1, len(accounts))
		// with opt
		accounts, err = db.GetAccounts(ctx, GetAccountsOptions{Keyword: "foo"})
		assert.NoError(t, err)
		assert.Equal(t, 2, len(accounts))
		// with opt and owner
		accounts, err = db.GetAccounts(ctx, GetAccountsOptions{Keyword: "hello", Owner: false})
		assert.NoError(t, err)
		assert.Equal(t, 1, len(accounts))
		// with not result
		accounts, err = db.GetAccounts(ctx, GetAccountsOptions{Keyword: "shiori"})
		assert.NoError(t, err)
		assert.Equal(t, 0, len(accounts))
	})
}

// TODO: Consider using `t.Parallel()` once we have automated database tests spawning databases using testcontainers.
func testUpdateBookmarkUpdatesModifiedTime(t *testing.T, db DB) {
	ctx := context.TODO()

	book := model.BookmarkDTO{
		URL:   "https://github.com/go-shiori/shiori",
		Title: "shiori",
	}

	resultBook, err := db.SaveBookmarks(ctx, true, book)
	assert.NoError(t, err, "Save bookmarks must not fail")

	updatedBook := resultBook[0]
	updatedBook.Title = "modified"
	updatedBook.ModifiedAt = ""

	time.Sleep(1 * time.Second)
	resultUpdatedBooks, err := db.SaveBookmarks(ctx, false, updatedBook)
	assert.NoError(t, err, "Save bookmarks must not fail")

	assert.NotEqual(t, resultBook[0].ModifiedAt, resultUpdatedBooks[0].ModifiedAt)
	assert.Equal(t, resultBook[0].CreatedAt, resultUpdatedBooks[0].CreatedAt)
	assert.Equal(t, resultBook[0].CreatedAt, resultBook[0].ModifiedAt)
	assert.NoError(t, err, "Get bookmarks must not fail")

	assert.Equal(t, updatedBook.Title, resultUpdatedBooks[0].Title, "Saved bookmark must have updated Title")
}

// TODO: Consider using `t.Parallel()` once we have automated database tests spawning databases using testcontainers.
func testGetBoomarksWithTimeFilters(t *testing.T, db DB) {
	ctx := context.TODO()

	book1 := model.BookmarkDTO{
		URL:   "https://github.com/go-shiori/shiori/one",
		Title: "Added First but Modified Last",
	}
	book2 := model.BookmarkDTO{
		URL:   "https://github.com/go-shiori/shiori/second",
		Title: "Added Last but Modified First",
	}

	// create two new bookmark
	resultBook1, err := db.SaveBookmarks(ctx, true, book1)
	assert.NoError(t, err, "Save bookmarks must not fail")
	time.Sleep(1 * time.Second)
	resultBook2, err := db.SaveBookmarks(ctx, true, book2)
	assert.NoError(t, err, "Save bookmarks must not fail")

	// update those bookmarks
	updatedBook1 := resultBook1[0]
	updatedBook1.Title = "Added First but Modified Last Updated Title"
	updatedBook1.ModifiedAt = ""

	updatedBook2 := resultBook2[0]
	updatedBook2.Title = "Last Added but modified First Updated Title"
	updatedBook2.ModifiedAt = ""

	// modified bookmark2 first after one second modified bookmark1
	resultUpdatedBook2, err := db.SaveBookmarks(ctx, false, updatedBook2)
	assert.NoError(t, err, "Save bookmarks must not fail")
	time.Sleep(1 * time.Second)
	resultUpdatedBook1, err := db.SaveBookmarks(ctx, false, updatedBook1)
	assert.NoError(t, err, "Save bookmarks must not fail")

	// get diffrent filteter combination
	booksOrderByLastAdded, err := db.GetBookmarks(ctx, GetBookmarksOptions{
		IDs:         []int{resultUpdatedBook1[0].ID, resultUpdatedBook2[0].ID},
		OrderMethod: 1,
	})
	assert.NoError(t, err, "Get bookmarks must not fail")
	booksOrderByLastModified, err := db.GetBookmarks(ctx, GetBookmarksOptions{
		IDs:         []int{resultUpdatedBook1[0].ID, resultUpdatedBook2[0].ID},
		OrderMethod: 2,
	})
	assert.NoError(t, err, "Get bookmarks must not fail")
	booksOrderById, err := db.GetBookmarks(ctx, GetBookmarksOptions{
		IDs:         []int{resultUpdatedBook1[0].ID, resultUpdatedBook2[0].ID},
		OrderMethod: 0,
	})
	assert.NoError(t, err, "Get bookmarks must not fail")

	// Check Last Added
	assert.Equal(t, booksOrderByLastAdded[0].Title, updatedBook2.Title)
	// Check Last Modified
	assert.Equal(t, booksOrderByLastModified[0].Title, updatedBook1.Title)
	// Second id should be 2 if order them by id
	assert.Equal(t, booksOrderById[1].ID, 2)
}
