//
//  tank
//
//  Import and Playlist Daemon for autoradio project
//
//
//  Copyright (C) 2017-2018 Christian Pointner <equinox@helsinki.at>
//
//  This file is part of tank.
//
//  tank is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  any later version.
//
//  tank is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with tank. If not, see <http://www.gnu.org/licenses/>.
//

package store

import (
	"fmt"
	"io"
	"os"
	"os/user"
	"path/filepath"
	"reflect"
	"testing"
	//"github.com/jinzhu/gorm"
)

var (
	testBasePath      = "run/aura-tank_testing"
	testPublicDirPath = filepath.Join(testBasePath, publicGroupName)
	testDBType        = "mysql"
	testDBConnection  = "tank:aura@tcp(127.0.0.1:3306)/tank?charset=utf8&parseTime=True&loc=Local"
	// testDBType       = "postgres"
	// testDBConnection = "host=127.0.0.1 port=5432 user=tank dbname=tank password=aura sslmode=disable"
	testGroup1      = "test1"
	testGroup2      = "test2"
	testUser1       = "user1"
	testUser2       = "user2"
	testSourceURI1  = "attachment://test1.mp3"
	testSourceURI2  = "attachment://test2.ogg"
	testFileArtist1 = "solo artist"
	testFileArtist2 = "band of 2"
	testFileAlbum1  = "first album"
	testFileAlbum2  = "second album"
	testFileTitle1  = "this is one title"
	testFileTitle2  = "this is not two title's"
)

func newTestStore(t *testing.T) *Store {
	cfg := Config{}
	cfg.BasePath = testBasePath
	cfg.DB.Type = testDBType
	cfg.DB.Connection = testDBConnection

	// TODO: drop database (all tables and constrains)

	store, err := NewStore(cfg)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	return store
}

func TestMain(m *testing.M) {
	u, err := user.Current()
	if err != nil {
		os.Exit(-1)
	}
	testBasePath = fmt.Sprintf("/run/user/%s/aura-tank_testing", u.Uid)
	os.RemoveAll(testBasePath)
	testPublicDirPath = filepath.Join(testBasePath, publicGroupName)
	//	testDBConnection = filepath.Join(testBasePath, ".db.sqlite")
	os.Exit(m.Run())
}

//
// Testing
//

func TestOpen(t *testing.T) {
	// base-path is non-existing directory
	cfg := Config{}
	cfg.BasePath = "/nonexistend/"
	cfg.DB.Type = testDBType
	cfg.DB.Connection = testDBConnection
	if _, err := NewStore(cfg); err == nil {
		t.Fatalf("opening store in nonexisting directory should throw an error")
	}
	cfg.BasePath = testBasePath
	if err := os.MkdirAll(testBasePath, 0700); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	/// exisiting but non-database file (only possible with sqlite...)
	// if f, err := os.Create(testDBConnection); err != nil {
	// 	t.Fatalf("unexpected error: %v", err)
	// } else {
	// 	io.WriteString(f, "this is not a sqlite db.")
	// 	f.Close()
	// }
	// if _, err := NewStore(cfg); err == nil {
	// 	t.Fatalf("opening store using a invalid database should throw an error")
	// }

	/// create new db and reopen it
	// os.Remove(testDBConnection)
	store, err := NewStore(cfg)
	if err != nil {
		t.Fatalf("creating new store failed: %v", err)
	}
	store.Close()

	if store, err = NewStore(cfg); err != nil {
		t.Fatalf("re-opening existing store failed: %v", err)
	}
	store.Close()

	if err := os.RemoveAll(testPublicDirPath); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if f, err := os.Create(testPublicDirPath); err != nil {
		t.Fatalf("unexpected error: %v", err)
	} else {
		io.WriteString(f, "this is not a directory")
		f.Close()
	}
	if _, err = NewStore(cfg); err == nil {
		t.Fatalf("opening store where path to public group dir is not a directory should throw an error")
	}
	if err := os.RemoveAll(testPublicDirPath); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestMigrations(t *testing.T) {
	cfg := Config{}
	cfg.BasePath = testBasePath
	cfg.DB.Type = testDBType
	cfg.DB.Connection = testDBConnection

	// os.Remove(testDBConnection)
	store, err := NewStore(cfg)
	if err != nil {
		t.Fatalf("creating new store failed: %v", err)
	}
	// TODO: This is only true for now!!
	if store.GetRevision() != "201806161853" {
		t.Fatalf("for now new databases should have revision %q: got %q", "201806161853", store.GetRevision())
	}
	store.Close()
	// TODO: needs more testing!!!!
}

//// Arrrrgh!!!!!
////  both database/sql and gorm are incredibly stupid!!!!
////  using database/sql alone it is needlessly hard to write portable sql queries:
////     postgres: db.Exec("INSERT INTO groups (name) VALUES ($1)", testGroup1)
////     mysql:    db.Exec("INSERT INTO groups (name) VALUES (?)", testGroup1)
////
////  using gorm db.Exec() will deal with that but i couldn't find a good way to get
////     the last inserted id without using the model code of gorm and we specifically
////     don't want to use the model code since it is not clear whether the constraints
////     are actually enforced by the DBMS or by some gorm callbacks...
////
// func TestDBConstraints(t *testing.T) {
// 	// we don't want to use the model but hand written SQL commands here since we want to check
// 	// whether the constraints are really enforced by the DBMS and not by some magic bullshit in gorm!
//  // This is even worse since gorm.AutoUpdate will not deal with foreign key constraints and we
//  // need do it ourselves at the migration scripts. It would be really nice to be able to check
//  // whether the migrations do the right thing!!!

// 	db, err := gorm.Open(testDBType, testDBConnection)
// 	if err != nil {
// 		t.Fatalf("unexpected error: %v", err)
// 	}
// 	if err = db.DB().Ping(); err != nil {
// 		t.Fatalf("unexpected error: %v", err)
// 	}

// 	res := db.Exec("INSERT INTO groups (name) VALUES (?)", testGroup1)
// 	if res.Error != nil {
// 		t.Fatalf("adding group '%s' shouldn't fail: %v", testGroup1, res.Error)
// 	}
// 	if res = db.Exec("INSERT INTO groups (name) VALUES (?)", testGroup1); res.Error == nil {
// 		t.Fatalf("re-adding the same group should fail")
// 	}

// 	if res = db.Exec("INSERT INTO files (group_name, size) VALUES (?, ?)", testGroup1, 500); res.Error != nil {
// 		t.Fatalf("re-adding the same group should fail")
// 	}
// }

// Groups
//

func checkGroups(t *testing.T, groups Groups, expected []string) {
	// if len(groups) != len(expected) {
	// 	t.Fatalf("expected %d groups in store but got %d: %v", len(expected), len(groups), groups)
	// }
	for _, gname := range expected {
		found := false
		for _, g := range groups {
			if gname == g.Name {
				found = true
				break
			}
		}
		if !found {
			t.Fatalf("expected group '%s' to be in store but got: %v", gname, groups)
		}
		if st, err := os.Stat(filepath.Join(testBasePath, gname)); err != nil {
			t.Fatalf("can't open group directory for group '%s': %v", gname, err)
		} else if !st.IsDir() {
			t.Fatalf("path of group '%s' is not a directory", gname)
		}
	}
}

func TestGroups(t *testing.T) {
	store := newTestStore(t)

	groups, err := store.ListGroups()
	if err != nil {
		t.Fatalf("listing groups failed: %v", err)
	}
	checkGroups(t, groups, []string{publicGroupName})

	if _, err = store.CreateGroup(testGroup1); err != nil {
		t.Fatalf("creating group failed: %v", err)
	}
	if _, err = store.CreateGroup(testGroup2); err != nil {
		t.Fatalf("creating group failed: %v", err)
	}

	groups, err = store.ListGroups()
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	checkGroups(t, groups, []string{publicGroupName, testGroup1, testGroup2})

	if err = store.DeleteGroup(testGroup1); err != nil {
		t.Fatalf("deleting group '%s' failed: %v", testGroup1, err)
	}

	groups, err = store.ListGroups()
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	checkGroups(t, groups, []string{publicGroupName, testGroup2})

	if err = store.DeleteGroup(testGroup2); err != nil {
		t.Fatalf("deleteing group '%s' failed: %v", testGroup2, err)
	}
	checkGroups(t, groups, []string{publicGroupName})
}

// Files
//

func TestFilesListCreateDelete(t *testing.T) {
	store := newTestStore(t)

	files, err := store.ListFiles(publicGroupName)
	if err != nil {
		t.Fatalf("listing files of public group failed: %v", err)
	}
	if len(files) != 0 {
		t.Fatalf("a newly created store should contain no files in public group but ListFiles returned: %v", files)
	}

	files, err = store.ListFiles("notexistend")
	if err != nil {
		t.Fatalf("listing files of not existing group shouldn't throw an error but returned: %v", err)
	}
	if len(files) != 0 {
		t.Fatalf("listing files of not existing group should return and empty list but ListFiles returned: %v", files)
	}

	publicFile, err := store.CreateFile(publicGroupName, File{})
	if err != nil {
		t.Fatalf("creating file in public group failed: %v", err)
	}

	files, err = store.ListFiles(publicGroupName)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(files) != 1 {
		t.Fatalf("ListFiles should return a single file but returned: %v", files)
	}

	_, err = store.CreateFile(testGroup1, File{Size: 17})
	if err != nil {
		t.Fatalf("creating file in not existing group shouldn't throw an error but CreateFile returned: %v", err)
	}
	_, err = store.CreateFile(testGroup1, File{Size: 23})
	if err != nil {
		t.Fatalf("creating file in not existing group shouldn't throw an error but CreateFile returned: %v", err)
	}

	groups, err := store.ListGroups()
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	checkGroups(t, groups, []string{publicGroupName, testGroup1})

	files, err = store.ListFiles(testGroup1)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(files) != 2 {
		t.Fatalf("ListFiles should return a two files  but returned: %v", files)
	}

	// clean up so next test can run with a clean DB, TODO: remove as soon as newTestStore() can re-init the DB
	if err = store.DeleteGroup(testGroup1); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if err = store.DeleteFile(publicGroupName, publicFile.ID); err != nil {
		t.Fatalf("deleteing file %d of group '%s' failed: %v", publicFile.ID, publicGroupName, err)
	}
	checkGroups(t, groups, []string{publicGroupName})
}

func fileEqual(a, b *File) bool {
	// TODO: comparing the whole file struct using DeepEqual does not work because it does not use time.Equal when comparing timestamps...
	return (a.Size == b.Size || reflect.DeepEqual(a.Source, b.Source) || reflect.DeepEqual(a.Metadata, b.Metadata))
}

func TestFilesCreateAndGet(t *testing.T) {
	store := newTestStore(t)

	if _, err := store.GetFile(testGroup1, 0); err != ErrNotFound {
		t.Fatalf("getting file in not-existing group should return ErrNotFound, but GetFile returned: %v", err)
	}

	file1 := File{Size: 12345}
	file1.Source.URI = testSourceURI1
	file1.Metadata.Artist = testFileArtist1
	file1.Metadata.Album = testFileAlbum1
	file1.Metadata.Title = testFileTitle1
	in1, err := store.CreateFile(testGroup1, file1)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	out1, err := store.GetFile(testGroup1, in1.ID)
	if err != nil {
		t.Fatalf("getting existing file from store shouldn't return an error, but GetFile returned: %v", err)
	}
	if !fileEqual(in1, out1) {
		t.Fatalf("GetFile returned different file than expected. Got %+v, expected %+v", out1, in1)
	}

	if _, err = store.GetFile(testGroup1, 0); err != ErrNotFound {
		t.Fatalf("getting not-existing file in existing group should return ErrNotFound, but GetFile returned: %v", err)
	}

	file2 := File{Size: 54321}
	file2.Source.URI = testSourceURI2
	file2.Metadata.Artist = testFileArtist2
	file2.Metadata.Album = testFileAlbum2
	file2.Metadata.Title = testFileTitle2
	in2, err := store.CreateFile(testGroup1, file2)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	out2, err := store.GetFile(testGroup1, in2.ID)
	if err != nil {
		t.Fatalf("getting existing file from store shouldn't return an error, but GetFile returned: %v", err)
	}
	if !fileEqual(in2, out2) {
		t.Fatalf("GetFile returned different file than expected. Got %+v, expected %+v", out2, in2)
	}

	files, err := store.ListFiles(testGroup1)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(files) != 2 {
		t.Fatalf("group '%s' should contain 2 files but ListFiles returned: %+v", testGroup1, files)
	}
	for _, file := range files {
		if file.ID == in1.ID {
			if !fileEqual(in1, out1) {
				t.Fatalf("ListFile returned different file than expected. Got %+v, expected %+v", out1, in1)
			}
		} else if file.ID == in2.ID {
			if !fileEqual(in2, out2) {
				t.Fatalf("ListFile returned different file than expected. Got %+v, expected %+v", out2, in2)
			}
		} else {
			t.Fatalf("group '%s' should only contain files %d and %d but ListFiles returned: %+v", testGroup1, in1.ID, in2.ID, files)
		}
	}

	// clean up so next test can run with a clean DB, TODO: remove as soon as newTestStore() can re-init the DB
	if err = store.DeleteGroup(testGroup1); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestFilesUpdate(t *testing.T) {
	store := newTestStore(t)

	if _, err := store.UpdateFile(publicGroupName, 0, File{}); err != ErrNotFound {
		t.Fatalf("updateting not-existing file hould return ErrNotFound, but UpdateFile returned: %v", err)
	}

	file := File{Size: 12345}
	file.Source.URI = testSourceURI1
	file.Metadata.Artist = testFileArtist1
	file.Metadata.Album = testFileAlbum1
	file.Metadata.Title = testFileTitle1
	in, err := store.CreateFile(testGroup2, file)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	out, err := store.GetFile(testGroup2, in.ID)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !fileEqual(in, out) {
		t.Fatalf("GetFile returned different file than expected. Got %+v, expected %+v", out, in)
	}

	out.Size = 54321
	out.Source.URI = testSourceURI2
	out.Metadata.Artist = testFileArtist2
	out.Metadata.Album = testFileAlbum2
	out.Metadata.Title = testFileTitle2
	if _, err = store.UpdateFile(testGroup2, in.ID, *out); err != nil {
		t.Fatalf("updateting an existing file shouldn't fail but UpdateFile returned: %v", err)
	}

	files, err := store.ListFiles(testGroup2)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(files) != 1 {
		t.Fatalf("group '%s' should contain 1 file but ListFiles returned: %+v", testGroup2, files)
	}
	if files[0].ID != in.ID || !fileEqual(out, &(files[0])) {
		t.Fatalf("ListFile returned different file than expected. Got %+v, expected %+v", out, files[0])
	}

	// clean up so next test can run with a clean DB, TODO: remove as soon as newTestStore() can re-init the DB
	if err = store.DeleteGroup(testGroup2); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestFilesDelete(t *testing.T) {
	store := newTestStore(t)

	if err := store.DeleteFile(testGroup1, 0); err != ErrNotFound {
		t.Fatalf("deleting not-existing file should return ErrNotFound, but DeleteFile returned: %v", err)
	}

	file, err := store.CreateFile(testGroup1, File{Size: 12345})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if err = store.DeleteFile(testGroup1, file.ID); err != nil {
		t.Fatalf("deleting file failed, DeleteFile returned: %v", err)
	}

	if err = store.DeleteFile(testGroup1, file.ID); err != ErrNotFound {
		t.Fatalf("repeated deletion of file should return ErrNotFound, but DeleteFile returned: %v", err)
	}

	// clean up so next test can run with a clean DB, TODO: remove as soon as newTestStore() can re-init the DB
	if err = store.DeleteGroup(testGroup1); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

// Playlists
//

func generateTestPlaylist(uris ...string) (p Playlist) {
	for _, u := range uris {
		e := PlaylistEntry{URI: u}
		p.Entries = append(p.Entries, e)
	}
	return p
}

func TestPlaylistsListCreateDelete(t *testing.T) {
	store := newTestStore(t)

	playlists, err := store.ListPlaylists(publicGroupName)
	if err != nil {
		t.Fatalf("listing playlists of public group failed: %v", err)
	}
	if len(playlists) != 0 {
		t.Fatalf("a newly created store should contain no playlists in public group but ListPlaylists returned: %v", playlists)
	}

	playlists, err = store.ListPlaylists("notexistend")
	if err != nil {
		t.Fatalf("listing playlists of not existing group shouldn't throw an error but returned: %v", err)
	}
	if len(playlists) != 0 {
		t.Fatalf("listing playlists of not existing group should return and empty list but ListPlaylists returned: %v", playlists)
	}

	in := generateTestPlaylist("audioin://1", "http://stream.example.com/live.mp")
	publicPlaylist, err := store.CreatePlaylist(publicGroupName, in)
	if err != nil {
		t.Fatalf("creating playlist in public group failed: %v", err)
	}

	playlists, err = store.ListPlaylists(publicGroupName)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(playlists) != 1 {
		t.Fatalf("ListPlaylists should return a single playlist but returned: %v", playlists)
	}

	in1 := generateTestPlaylist("audioin://1", "http://stream.example.com/live.mp3")
	_, err = store.CreatePlaylist(testGroup1, in1)
	if err != nil {
		t.Fatalf("creating playlist in not existing group shouldn't throw an error but CreatePlaylist returned: %v", err)
	}
	in2 := generateTestPlaylist("https://stream.example.com/other.ogg", "audioin://2")
	_, err = store.CreatePlaylist(testGroup1, in2)
	if err != nil {
		t.Fatalf("creating playlist in not existing group shouldn't throw an error but CreatePlaylist returned: %v", err)
	}

	groups, err := store.ListGroups()
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	checkGroups(t, groups, []string{publicGroupName, testGroup1})

	playlists, err = store.ListPlaylists(testGroup1)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(playlists) != 2 {
		t.Fatalf("ListPlaylists should return two playlists but returned: %v", playlists)
	}

	// clean up so next test can run with a clean DB, TODO: remove as soon as newTestStore() can re-init the DB
	if err = store.DeleteGroup(testGroup1); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if err = store.DeletePlaylist(publicGroupName, publicPlaylist.ID); err != nil {
		t.Fatalf("deleteing playlist %d of group '%s' failed: %v", publicPlaylist.ID, publicGroupName, err)
	}
}

func TestPlaylistsCreateAndGet(t *testing.T) {
	store := newTestStore(t)

	f := File{Size: 12345}
	f.Source.URI = testSourceURI1
	f.Metadata.Artist = testFileArtist1
	f.Metadata.Album = testFileAlbum1
	f.Metadata.Title = testFileTitle1
	file1, err := store.CreateFile(testGroup1, f)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if _, err := store.GetPlaylist(testGroup1, 0); err != ErrNotFound {
		t.Fatalf("getting playlist in not-existing group should return ErrNotFound, but GetPlaylist returned: %v", err)
	}

	p := generateTestPlaylist("http://stream.example.com/stream.mp3")
	p.Entries = append(p.Entries, PlaylistEntry{File: &File{GroupName: file1.GroupName, ID: file1.ID}})
	list1, err := store.CreatePlaylist(testGroup1, p)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if _, err := store.GetPlaylist(testGroup1, list1.ID); err != nil {
		t.Fatalf("getting existing playlist from store shouldn't return an error, but GetPlaylist returned: %v", err)
	}
	// TODO: check if playlists are equal

	p = generateTestPlaylist("http://stream.example.com/other.mp3", fmt.Sprintf("file://%s/%d", file1.GroupName, file1.ID))
	if _, err = store.CreatePlaylist(testGroup1, p); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	playlists, err := store.ListPlaylists(testGroup1)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(playlists) != 2 {
		t.Fatalf("ListPlaylists should return two playlists but returned: %v", playlists)
	}
	// TODO: check playlists contains both lists
}