package extension_repo

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"seanime/internal/api/anilist"
	"seanime/internal/api/metadata"
	"seanime/internal/continuity"
	"seanime/internal/database/db"
	"seanime/internal/events"
	"seanime/internal/extension"
	"seanime/internal/goja/goja_runtime"
	"seanime/internal/hook"
	"seanime/internal/library/fillermanager"
	"seanime/internal/library/playbackmanager"
	"seanime/internal/mediaplayers/mediaplayer"
	"seanime/internal/mediaplayers/mpv"
	"seanime/internal/platforms/anilist_platform"
	"seanime/internal/plugin"
	"seanime/internal/test_utils"
	"seanime/internal/util"
	"seanime/internal/util/filecache"
	"testing"
	"time"

	"github.com/rs/zerolog"
	"github.com/samber/lo"
	"github.com/stretchr/testify/require"
)

var (
	testDocumentsDir          = "/Users/rahim/Documents"
	testDocumentCollectionDir = "/Users/rahim/Documents/collection"
	testVideoPath             = "/Users/rahim/Documents/collection/Bocchi the Rock/[ASW] Bocchi the Rock! - 01 [1080p HEVC][EDC91675].mkv"

	tempTestDir = "$TEMP/test"
)

// TestPluginOptions contains options for initializing a test plugin
type TestPluginOptions struct {
	ID          string
	Payload     string
	Language    extension.Language
	Permissions extension.PluginPermissions
	PoolSize    int
	SetupHooks  bool
}

// DefaultTestPluginOptions returns default options for a test plugin
func DefaultTestPluginOptions() TestPluginOptions {
	return TestPluginOptions{
		ID:          "dummy-plugin",
		Payload:     "",
		Language:    extension.LanguageJavascript,
		Permissions: extension.PluginPermissions{},
		PoolSize:    15,
		SetupHooks:  true,
	}
}

// InitTestPlugin initializes a test plugin with the given options
func InitTestPlugin(t testing.TB, opts TestPluginOptions) (*GojaPlugin, *zerolog.Logger, *goja_runtime.Manager, *anilist_platform.AnilistPlatform, events.WSEventManagerInterface, error) {
	if opts.SetupHooks {
		test_utils.SetTwoLevelDeep()
		if tPtr, ok := t.(*testing.T); ok {
			test_utils.InitTestProvider(tPtr, test_utils.Anilist())
		}
	}

	ext := &extension.Extension{
		ID:       opts.ID,
		Payload:  opts.Payload,
		Language: opts.Language,
		Plugin:   &extension.PluginManifest{},
	}

	if len(opts.Permissions.Scopes) > 0 {
		ext.Plugin = &extension.PluginManifest{
			Permissions: opts.Permissions,
		}
	}

	ext.Plugin.Permissions.Allow = opts.Permissions.Allow

	logger := util.NewLogger()
	wsEventManager := events.NewMockWSEventManager(logger)
	anilistClient := anilist.NewMockAnilistClient()
	anilistPlatform := anilist_platform.NewAnilistPlatform(anilistClient, logger).(*anilist_platform.AnilistPlatform)

	// Initialize hook manager if needed
	if opts.SetupHooks {
		hm := hook.NewHookManager(hook.NewHookManagerOptions{Logger: logger})
		hook.SetGlobalHookManager(hm)
	}

	manager := goja_runtime.NewManager(logger)

	database, err := db.NewDatabase(test_utils.ConfigData.Path.DataDir, test_utils.ConfigData.Database.Name, logger)
	require.NoError(t, err)

	plugin.GlobalAppContext.SetModulesPartial(plugin.AppContextModules{
		Database:          database,
		AnilistPlatform:   anilistPlatform,
		WSEventManager:    wsEventManager,
		AnimeLibraryPaths: &[]string{},
		PlaybackManager:   &playbackmanager.PlaybackManager{},
	})

	plugin, _, err := NewGojaPlugin(ext, opts.Language, logger, manager, wsEventManager)
	return plugin, logger, manager, anilistPlatform, wsEventManager, err
}

/////////////////////////////////////////////////////////////////////////////////////////////

func TestGojaPluginAnime(t *testing.T) {
	payload := `
	function init() {

		$ui.register(async (ctx) => {
			try {
				console.log("Fetching anime entry");
				console.log(typeof ctx.anime.getAnimeEntry)
ctx.anime.getAnimeEntry(21)
				//ctx.anime.getAnimeEntry(21).then((anime) => {
				//	console.log("Anime", anime)
				//}).catch((e) => {
				//	console.error("Error fetching anime entry", e)
				//})
			} catch (e) {
				console.error("Error fetching anime entry", e)
			}
		})
	}
	`

	opts := DefaultTestPluginOptions()
	opts.Payload = payload
	opts.Permissions = extension.PluginPermissions{
		Scopes: []extension.PluginPermissionScope{
			extension.PluginPermissionAnilist,
			extension.PluginPermissionDatabase,
		},
	}
	logger := util.NewLogger()
	database, err := db.NewDatabase(test_utils.ConfigData.Path.DataDir, test_utils.ConfigData.Database.Name, logger)
	require.NoError(t, err)

	metadataProvider := metadata.NewProvider(&metadata.NewProviderImplOptions{
		FileCacher: lo.Must(filecache.NewCacher(t.TempDir())),
		Logger:     logger,
	})

	fillerManager := fillermanager.New(&fillermanager.NewFillerManagerOptions{
		Logger: logger,
		DB:     database,
	})

	plugin.GlobalAppContext.SetModulesPartial(plugin.AppContextModules{
		Database:         database,
		MetadataProvider: metadataProvider,
		FillerManager:    fillerManager,
	})

	_, logger, manager, _, _, err := InitTestPlugin(t, opts)
	require.NoError(t, err)

	manager.PrintPluginPoolMetrics(opts.ID)

	time.Sleep(3 * time.Second)
}

/////////////////////////////////////////////////////////////////////////////////////////////

func TestGojaPluginMpv(t *testing.T) {
	payload := fmt.Sprintf(`
function init() {

	$ui.register(async (ctx) => {

		console.log("Testing MPV");

		await ctx.mpv.openAndPlay("%s")

		const cancel = ctx.mpv.onEvent((event) => {
			console.log("Event received", event)
		})

		ctx.setTimeout(() => {
			const conn = ctx.mpv.getConnection()
			if (conn) {
				conn.call("set_property", "pause", true)
			}
		}, 3000)

		ctx.setTimeout(async () => {
			console.log("Cancelling event listener")
			cancel()
			await ctx.mpv.stop()
		}, 5000)
	});

}
	`, testVideoPath)

	playbackManager, _, err := getPlaybackManager(t)
	require.NoError(t, err)

	plugin.GlobalAppContext.SetModulesPartial(plugin.AppContextModules{
		PlaybackManager: playbackManager,
	})

	opts := DefaultTestPluginOptions()
	opts.Payload = payload
	opts.Permissions = extension.PluginPermissions{
		Scopes: []extension.PluginPermissionScope{
			extension.PluginPermissionPlayback,
		},
	}

	_, _, manager, _, _, err := InitTestPlugin(t, opts)
	require.NoError(t, err)

	manager.PrintPluginPoolMetrics(opts.ID)

	time.Sleep(8 * time.Second)
}

/////////////////////////////////////////////////////////////////////////////////////////////

// Test that the plugin cannot access paths that are not allowed
// $os.readDir should throw an error
func TestGojaPluginPathNotAllowed(t *testing.T) {
	payload := fmt.Sprintf(`
function init() {
	$ui.register((ctx) => {

		const tempDir = $os.tempDir();
		console.log("Temp dir", tempDir);

		const dirPath = "%s";
		const entries = $os.readDir(dirPath);
	});
}
	`, testDocumentCollectionDir)

	opts := DefaultTestPluginOptions()
	opts.Payload = payload
	opts.Permissions = extension.PluginPermissions{
		Scopes: []extension.PluginPermissionScope{
			extension.PluginPermissionSystem,
		},
		Allow: extension.PluginAllowlist{
			ReadPaths:  []string{"$TEMP/*", testDocumentsDir},
			WritePaths: []string{"$TEMP/*"},
		},
	}

	_, _, manager, _, _, err := InitTestPlugin(t, opts)
	require.Error(t, err)

	manager.PrintPluginPoolMetrics(opts.ID)

}

/////////////////////////////////////////////////////////////////////////////////////////////

// Test that the plugin can play a video and listen to events
func TestGojaPluginPlaybackEvents(t *testing.T) {
	payload := fmt.Sprintf(`
function init() {

	$ui.register((ctx) => {
		console.log("Testing Playback");

		const cancel = ctx.playback.registerEventListener("mySubscriber", (event) => {
			console.log("Event received", event)
		})

		ctx.playback.playUsingMediaPlayer("%s")

		ctx.setTimeout(() => {
			console.log("Cancelling event listener")
			cancel()
		}, 15000)
	});

}
	`, testVideoPath)

	playbackManager, _, err := getPlaybackManager(t)
	require.NoError(t, err)

	plugin.GlobalAppContext.SetModulesPartial(plugin.AppContextModules{
		PlaybackManager: playbackManager,
	})

	opts := DefaultTestPluginOptions()
	opts.Payload = payload
	opts.Permissions = extension.PluginPermissions{
		Scopes: []extension.PluginPermissionScope{
			extension.PluginPermissionPlayback,
		},
	}

	_, _, manager, _, _, err := InitTestPlugin(t, opts)
	require.NoError(t, err)

	manager.PrintPluginPoolMetrics(opts.ID)

	time.Sleep(16 * time.Second)
}

/////////////////////////////////////////////////////////////////////////////////////////////

// Tests that we can register hooks and the UI handler.
// Tests that the state updates correctly and effects run as expected.
// Tests that we can fetch data from an external source.
func TestGojaPluginUIAndHooks(t *testing.T) {
	// Create a test server
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		time.Sleep(1000 * time.Millisecond)
		fmt.Fprint(w, `{"test": "data"}`)
	}))
	defer server.Close()

	payload := fmt.Sprintf(`
	function init() {

		$app.onGetAnime(async (e) => {
			const url = "%s"

			// const res = $await(fetch(url))
			const res = await fetch(url)
			const data = res.json()
			console.log("fetched results in hook", data)
			$store.set("data", data)

			console.log("first hook fired");

			e.next();
		});

		$app.onGetAnime(async (e) => {
			console.log("results from first hook", $store.get("data"));

			e.next();
		});

		$ui.register((ctx) => {
			const url = "%s"
			console.log("this is the start");

			const count = ctx.state(0)

			ctx.effect(async () => {
				console.log("running effect that takes 1s")
				ctx.setTimeout(() => {
					console.log("1s elapsed since first effect called")
				}, 1000)
				const [a, b, c, d, e, f] = await Promise.all([
					ctx.fetch("https://jsonplaceholder.typicode.com/todos/1"),
					ctx.fetch("https://jsonplaceholder.typicode.com/todos/2"),
					ctx.fetch("https://jsonplaceholder.typicode.com/todos/3"),
					ctx.fetch("https://jsonplaceholder.typicode.com/todos/3"),
					ctx.fetch("https://jsonplaceholder.typicode.com/todos/3"),
					ctx.fetch(url),
				])
				console.log("fetch results", a.json(), b.json(), c.json(), d.json(), e.json(), f.json())
			}, [count])

			ctx.effect(() => {
				console.log("running effect that runs fast ran second")
			}, [count])

			count.set(p => p+1)

			console.log("this is the end");
		});

	}
	`, server.URL, server.URL)

	opts := DefaultTestPluginOptions()
	opts.Payload = payload

	_, _, manager, anilistPlatform, _, err := InitTestPlugin(t, opts)
	require.NoError(t, err)

	go func() {
		time.Sleep(time.Second)
		_, err := anilistPlatform.GetAnime(t.Context(), 178022)
		if err != nil {
			t.Errorf("GetAnime returned error: %v", err)
		}

		// _, err = anilistPlatform.GetAnime(177709)
		// if err != nil {
		// 	t.Errorf("GetAnime returned error: %v", err)
		// }
	}()

	manager.PrintPluginPoolMetrics(opts.ID)

	time.Sleep(3 * time.Second)
}

/////////////////////////////////////////////////////////////////////////////////////////////

func TestGojaPluginStore(t *testing.T) {
	payload := `
	function init() {

		$app.onGetAnime((e) => {

			$store.set("anime", e.anime);
			$store.set("value", 42);

			e.next();
		});

		$app.onGetAnime((e) => {

			console.log("Hook 2, value", $store.get("value"));
			console.log("Hook 2, value 2", $store.get("value2"));

			e.next();
		});

	}
	`

	opts := DefaultTestPluginOptions()
	opts.Payload = payload

	plugin, _, manager, anilistPlatform, _, err := InitTestPlugin(t, opts)
	require.NoError(t, err)

	m, err := anilistPlatform.GetAnime(t.Context(), 178022)
	if err != nil {
		t.Fatalf("GetAnime returned error: %v", err)
	}

	util.Spew(m.Title)
	util.Spew(m.Synonyms)

	m, err = anilistPlatform.GetAnime(t.Context(), 177709)
	if err != nil {
		t.Fatalf("GetAnime returned error: %v", err)
	}

	util.Spew(m.Title)

	value := plugin.store.Get("value")
	require.NotNil(t, value)

	manager.PrintPluginPoolMetrics(opts.ID)
}

/////////////////////////////////////////////////////////////////////////////////////////////

func TestGojaPluginJsonFieldNames(t *testing.T) {
	payload := `
	function init() {

		$app.onPreUpdateEntryProgress((e) => {
			console.log("pre update entry progress", e)

			$store.set("mediaId", e.mediaId);

			e.next();
		});

	}
	`

	opts := DefaultTestPluginOptions()
	opts.Payload = payload

	plugin, _, manager, anilistPlatform, _, err := InitTestPlugin(t, opts)
	require.NoError(t, err)

	err = anilistPlatform.UpdateEntryProgress(t.Context(), 178022, 1, lo.ToPtr(1))
	if err != nil {
		t.Fatalf("GetAnime returned error: %v", err)
	}

	mediaId := plugin.store.Get("mediaId")
	require.NotNil(t, mediaId)

	manager.PrintPluginPoolMetrics(opts.ID)
}

/////////////////////////////////////////////////////////////////////////////////////////////

func TestGojaPluginAnilistCustomQuery(t *testing.T) {
	payload := `
	function init() {
		$ui.register((ctx) => {
		const token = $database.anilist.getToken()
			try {
				const res = $anilist.customQuery({ query:` + "`" + `
					query GetOnePiece {
						Media(id: 21) {
							title {
								romaji
								english
								native
								userPreferred
							}
							airingSchedule(perPage: 1, page: 1) {
								nodes {
									episode
									airingAt
								}
							}
							id
						}
					}
				` + "`" + `, variables: {}}, token);

				console.log("res", res)
			} catch (e) {
				console.error("Error fetching anime list", e);
			}
		});
	}
	`

	opts := DefaultTestPluginOptions()
	opts.Payload = payload

	opts.Permissions = extension.PluginPermissions{
		Scopes: []extension.PluginPermissionScope{
			extension.PluginPermissionAnilist,
			extension.PluginPermissionAnilistToken,
			extension.PluginPermissionDatabase,
		},
	}

	plugin, _, manager, _, _, err := InitTestPlugin(t, opts)
	require.NoError(t, err)

	_ = plugin

	manager.PrintPluginPoolMetrics(opts.ID)
}

/////////////////////////////////////////////////////////////////////////////////////////////

func TestGojaPluginAnilistListAnime(t *testing.T) {
	payload := `
	function init() {

		$ui.register((ctx) => {
		
		try {
			const res = $anilist.listRecentAnime(1, 15, undefined, undefined, undefined)
			console.log("res", res)
		} catch (e) {
			console.error("Error fetching anime list", e)
		}

        })
	}
	`
	opts := DefaultTestPluginOptions()
	opts.Payload = payload

	opts.Permissions = extension.PluginPermissions{
		Scopes: []extension.PluginPermissionScope{
			extension.PluginPermissionAnilist,
		},
	}

	plugin, _, manager, _, _, err := InitTestPlugin(t, opts)
	require.NoError(t, err)

	_ = plugin

	manager.PrintPluginPoolMetrics(opts.ID)
}

/////////////////////////////////////////////////////////////////////////////////////////////

func TestGojaPluginStorage(t *testing.T) {
	payload := `
	function init() {

		$app.onGetAnime((e) => {

			if ($storage.get("foo") !== "qux") {
				throw new Error("foo should be qux")
			}

			$storage.set("foo", "anime")
			console.log("foo", $storage.get("foo"))
			$store.set("expectedValue4", "anime")

			e.next();
		});

		$ui.register((ctx) => {
			
			$storage.set("foo", "bar")
			console.log("foo", $storage.get("foo"))

			$store.set("expectedValue1", "bar")

			ctx.setTimeout(() => {
				console.log("foo", $storage.get("foo"))
				$storage.set("foo", "baz")
				console.log("foo", $storage.get("foo"))
				$store.set("expectedValue2", "baz")
			}, 1000)

			ctx.setTimeout(() => {
				console.log("foo", $storage.get("foo"))
				$storage.set("foo", "qux")
				console.log("foo", $storage.get("foo"))
				$store.set("expectedValue3", "qux")
			}, 1500)
			
		})

	}
	`

	opts := DefaultTestPluginOptions()
	opts.Payload = payload
	opts.Permissions = extension.PluginPermissions{
		Scopes: []extension.PluginPermissionScope{
			extension.PluginPermissionDatabase,
			extension.PluginPermissionStorage,
		},
	}

	plugin, _, manager, anilistPlatform, _, err := InitTestPlugin(t, opts)
	require.NoError(t, err)

	_ = plugin

	manager.PrintPluginPoolMetrics(opts.ID)

	time.Sleep(2 * time.Second)

	_, err = anilistPlatform.GetAnime(t.Context(), 178022)
	require.NoError(t, err)

	expectedValue1 := plugin.store.Get("expectedValue1")
	require.Equal(t, "bar", expectedValue1)

	expectedValue2 := plugin.store.Get("expectedValue2")
	require.Equal(t, "baz", expectedValue2)

	expectedValue3 := plugin.store.Get("expectedValue3")
	require.Equal(t, "qux", expectedValue3)

	expectedValue4 := plugin.store.Get("expectedValue4")
	require.Equal(t, "anime", expectedValue4)

}

/////////////////////////////////////////////////////////////////////////////////////////////

func TestGojaPluginTryCatch(t *testing.T) {
	payload := `
	function init() {

		$ui.register((ctx) => {
			try {
				throw new Error("test error")
			} catch (e) {
				console.log("catch", e)
				$store.set("error", e)
			}

			try {
				undefined.f()
			} catch (e) {
				console.log("catch 2", e)
				$store.set("error2", e)
			}
			
		})

	}
	`

	opts := DefaultTestPluginOptions()
	opts.Payload = payload

	plugin, _, manager, _, _, err := InitTestPlugin(t, opts)
	require.NoError(t, err)

	manager.PrintPluginPoolMetrics(opts.ID)

	err1 := plugin.store.Get("error")
	require.NotNil(t, err1)

	err2 := plugin.store.Get("error2")
	require.NotNil(t, err2)
}

/////////////////////////////////////////////////////////////////////////////////////////////

func TestGojaSharedMemory(t *testing.T) {
	payload := `
	function init() {

		$ui.register((ctx) => {
			const state = ctx.state("test")

			$store.set("state", state)
			
		})

		$app.onGetAnime((e) => {
			const state = $store.get("state")
			console.log("state", state)
			console.log("state value", state.get())
			e.next();
		})

	}
	`

	opts := DefaultTestPluginOptions()
	opts.Payload = payload

	plugin, _, manager, anilistPlatform, _, err := InitTestPlugin(t, opts)
	require.NoError(t, err)
	_ = plugin

	manager.PrintPluginPoolMetrics(opts.ID)

	_, err = anilistPlatform.GetAnime(t.Context(), 178022)
	require.NoError(t, err)

	time.Sleep(2 * time.Second)
}

/////////////////////////////////////////////////////////////////////////////////////////////s

func getPlaybackManager(t *testing.T) (*playbackmanager.PlaybackManager, *anilist.AnimeCollection, error) {

	logger := util.NewLogger()

	wsEventManager := events.NewMockWSEventManager(logger)

	database, err := db.NewDatabase(test_utils.ConfigData.Path.DataDir, test_utils.ConfigData.Database.Name, logger)

	if err != nil {
		t.Fatalf("error while creating database, %v", err)
	}

	filecacher, err := filecache.NewCacher(t.TempDir())
	require.NoError(t, err)
	anilistClient := anilist.TestGetMockAnilistClient()
	anilistPlatform := anilist_platform.NewAnilistPlatform(anilistClient, logger)
	animeCollection, err := anilistPlatform.GetAnimeCollection(t.Context(), true)
	metadataProvider := metadata.GetMockProvider(t)
	require.NoError(t, err)
	continuityManager := continuity.NewManager(&continuity.NewManagerOptions{
		FileCacher: filecacher,
		Logger:     logger,
		Database:   database,
	})

	playbackManager := playbackmanager.New(&playbackmanager.NewPlaybackManagerOptions{
		WSEventManager:   wsEventManager,
		Logger:           logger,
		Platform:         anilistPlatform,
		MetadataProvider: metadataProvider,
		Database:         database,
		RefreshAnimeCollectionFunc: func() {
			// Do nothing
		},
		DiscordPresence:   nil,
		IsOffline:         lo.ToPtr(false),
		ContinuityManager: continuityManager,
	})

	playbackManager.SetAnimeCollection(animeCollection)
	playbackManager.SetSettings(&playbackmanager.Settings{
		AutoPlayNextEpisode: false,
	})

	playbackManager.SetMediaPlayerRepository(mediaplayer.NewRepository(&mediaplayer.NewRepositoryOptions{
		Mpv:               mpv.New(logger, "", ""),
		Logger:            logger,
		Default:           "mpv",
		ContinuityManager: continuityManager,
	}))

	return playbackManager, animeCollection, nil
}
