package post

import (
	"sort"
	"strconv"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/tilinna/clock"
)

func TestNewID(t *testing.T) {

	tests := [][2]string{
		{
			"Why Do We Have WiFi Passwords?",
			"why-do-we-have-wifi-passwords",
		},
		{
			"Ginger: A Small VM Update",
			"ginger-a-small-vm-update",
		},
		{
			"Something-Weird.... woah!",
			"somethingweird-woah",
		},
	}

	for i, test := range tests {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			assert.Equal(t, test[1], NewID(test[0]))
		})
	}
}

func testPost(i int) Post {
	istr := strconv.Itoa(i)
	return Post{
		ID:    istr,
		Title: istr,
		Body:  istr,
	}
}

type storeTestHarness struct {
	clock *clock.Mock
	store Store
}

func newStoreTestHarness(t *testing.T) storeTestHarness {

	clock := clock.NewMock(time.Now().UTC().Truncate(1 * time.Hour))

	db := NewInMemSQLDB()
	t.Cleanup(func() { db.Close() })

	store := NewStore(db)

	return storeTestHarness{
		clock: clock,
		store: store,
	}
}

func (h *storeTestHarness) testStoredPost(i int) StoredPost {
	post := testPost(i)
	return StoredPost{
		Post:        post,
		PublishedAt: h.clock.Now(),
	}
}

func TestStore(t *testing.T) {

	assertPostEqual := func(t *testing.T, exp, got StoredPost) {
		t.Helper()
		sort.Strings(exp.Tags)
		sort.Strings(got.Tags)
		assert.Equal(t, exp, got)
	}

	assertPostsEqual := func(t *testing.T, exp, got []StoredPost) {
		t.Helper()

		if !assert.Len(t, got, len(exp), "exp:%+v\ngot: %+v", exp, got) {
			return
		}

		for i := range exp {
			assertPostEqual(t, exp[i], got[i])
		}
	}

	t.Run("not_found", func(t *testing.T) {
		h := newStoreTestHarness(t)

		_, err := h.store.GetByID("foo")
		assert.ErrorIs(t, err, ErrPostNotFound)
	})

	t.Run("set_get_delete", func(t *testing.T) {
		h := newStoreTestHarness(t)

		now := h.clock.Now().UTC()

		post := testPost(0)
		post.Tags = []string{"foo", "bar"}

		first, err := h.store.Set(post, now)
		assert.NoError(t, err)
		assert.True(t, first)

		gotPost, err := h.store.GetByID(post.ID)
		assert.NoError(t, err)

		assertPostEqual(t, StoredPost{
			Post:        post,
			PublishedAt: now,
		}, gotPost)

		// we will now try updating the post on a different day, and ensure it
		// updates properly

		h.clock.Add(24 * time.Hour)
		newNow := h.clock.Now().UTC()

		post.Title = "something else"
		post.Description = "some description"
		post.Series = "whatever"
		post.Body = "anything"
		post.Tags = []string{"bar", "baz"}

		first, err = h.store.Set(post, newNow)
		assert.NoError(t, err)
		assert.False(t, first)

		gotPost, err = h.store.GetByID(post.ID)
		assert.NoError(t, err)

		assertPostEqual(t, StoredPost{
			Post:          post,
			PublishedAt:   now,
			LastUpdatedAt: newNow,
		}, gotPost)

		// delete the post, it should go away
		assert.NoError(t, h.store.Delete(post.ID))

		_, err = h.store.GetByID(post.ID)
		assert.ErrorIs(t, err, ErrPostNotFound)
	})

	t.Run("get", func(t *testing.T) {
		h := newStoreTestHarness(t)

		now := h.clock.Now().UTC()

		posts := []StoredPost{
			h.testStoredPost(3),
			h.testStoredPost(2),
			h.testStoredPost(1),
			h.testStoredPost(0),
		}

		for _, post := range posts {
			_, err := h.store.Set(post.Post, now)
			assert.NoError(t, err)
		}

		gotPosts, hasMore, err := h.store.Get(0, 2)
		assert.NoError(t, err)
		assert.True(t, hasMore)
		assertPostsEqual(t, posts[:2], gotPosts)

		gotPosts, hasMore, err = h.store.Get(1, 2)
		assert.NoError(t, err)
		assert.False(t, hasMore)
		assertPostsEqual(t, posts[2:4], gotPosts)

		posts = append([]StoredPost{h.testStoredPost(4)}, posts...)
		_, err = h.store.Set(posts[0].Post, now)
		assert.NoError(t, err)

		gotPosts, hasMore, err = h.store.Get(1, 2)
		assert.NoError(t, err)
		assert.True(t, hasMore)
		assertPostsEqual(t, posts[2:4], gotPosts)

		gotPosts, hasMore, err = h.store.Get(2, 2)
		assert.NoError(t, err)
		assert.False(t, hasMore)
		assertPostsEqual(t, posts[4:], gotPosts)
	})

	t.Run("get_by_series", func(t *testing.T) {
		h := newStoreTestHarness(t)

		now := h.clock.Now().UTC()

		posts := []StoredPost{
			h.testStoredPost(3),
			h.testStoredPost(2),
			h.testStoredPost(1),
			h.testStoredPost(0),
		}

		posts[0].Series = "foo"
		posts[1].Series = "bar"
		posts[2].Series = "bar"

		for _, post := range posts {
			_, err := h.store.Set(post.Post, now)
			assert.NoError(t, err)
		}

		fooPosts, err := h.store.GetBySeries("foo")
		assert.NoError(t, err)
		assertPostsEqual(t, posts[:1], fooPosts)

		barPosts, err := h.store.GetBySeries("bar")
		assert.NoError(t, err)
		assertPostsEqual(t, posts[1:3], barPosts)

		bazPosts, err := h.store.GetBySeries("baz")
		assert.NoError(t, err)
		assert.Empty(t, bazPosts)
	})

	t.Run("get_by_tag", func(t *testing.T) {

		h := newStoreTestHarness(t)

		now := h.clock.Now().UTC()

		posts := []StoredPost{
			h.testStoredPost(3),
			h.testStoredPost(2),
			h.testStoredPost(1),
			h.testStoredPost(0),
		}

		posts[0].Tags = []string{"foo"}
		posts[1].Tags = []string{"foo", "bar"}
		posts[2].Tags = []string{"bar"}

		for _, post := range posts {
			_, err := h.store.Set(post.Post, now)
			assert.NoError(t, err)
		}

		fooPosts, err := h.store.GetByTag("foo")
		assert.NoError(t, err)
		assertPostsEqual(t, posts[:2], fooPosts)

		barPosts, err := h.store.GetByTag("bar")
		assert.NoError(t, err)
		assertPostsEqual(t, posts[1:3], barPosts)

		bazPosts, err := h.store.GetByTag("baz")
		assert.NoError(t, err)
		assert.Empty(t, bazPosts)

		tags, err := h.store.GetTags()
		assert.NoError(t, err)
		assert.ElementsMatch(t, []string{"foo", "bar"}, tags)
	})
}
