package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"os/signal"

	"dagger.io/dagger"
	"github.com/grafana/grafana/pkg/build/e2eutil"
	"github.com/urfave/cli/v3"
)

var (
	grafanaPort = 3001
)

func main() {
	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
	defer cancel()

	if err := NewApp().Run(ctx, os.Args); err != nil {
		cancel()
		fmt.Println(err)
		os.Exit(1)
	}
}

func NewApp() *cli.Command {
	return &cli.Command{
		Name:  "a11y",
		Usage: "Run Grafana playwright e2e tests",
		Flags: []cli.Flag{
			&cli.StringFlag{
				Name:      "grafana-dir",
				Usage:     "Path to the grafana/grafana clone directory",
				Value:     ".",
				Validator: mustBeDir("grafana-dir", false, false),
				TakesFile: true,
			},
			&cli.StringFlag{
				Name:      "package",
				Usage:     "Path to the grafana tar.gz package",
				Value:     "grafana.tar.gz",
				Validator: mustBeFile("package", false),
				TakesFile: true,
			},
			&cli.StringFlag{
				Name:      "license",
				Usage:     "Path to the Grafana Enterprise license file (optional)",
				Validator: mustBeFile("license", true),
				TakesFile: true,
			},
			&cli.StringFlag{
				Name:  "shard",
				Usage: "Test shard to run. See Playwright docs",
			},
			&cli.StringFlag{
				Name:      "results-dir",
				Usage:     "Path to the directory to export the playwright test results to (optional)",
				Validator: mustBeDir("results-dir", true, true),
			},
			&cli.StringFlag{
				Name:      "html-dir",
				Usage:     "Enables the HTML reporter, exported to this directory (optional)",
				Validator: mustBeDir("html-dir", true, true),
			},
			&cli.StringFlag{
				Name:      "blob-dir",
				Usage:     "Enables the blob reporter, exported to this directory. Useful with --shard (optional)",
				Validator: mustBeDir("blob-dir", true, true),
			},
			&cli.StringFlag{
				Name:  "playwright-command",
				Usage: "The playwright command to run.",
				Value: "yarn e2e:playwright",
			},
			&cli.StringFlag{
				Name:      "cloud-plugin-creds",
				Usage:     "Path to the cloud plugin credentials file (only required for running @cloud-plugins e2e tests)",
				Validator: mustBeFile("cloud-plugin-creds", true),
				TakesFile: true,
			},
		},
		Action: run,
	}
}

func run(ctx context.Context, cmd *cli.Command) error {
	grafanaDir := cmd.String("grafana-dir")
	targzPath := cmd.String("package")
	licensePath := cmd.String("license")
	cloudPluginCredsPath := cmd.String("cloud-plugin-creds")
	pwShard := cmd.String("shard")
	resultsDir := cmd.String("results-dir")
	htmlDir := cmd.String("html-dir")
	blobDir := cmd.String("blob-dir")
	playwrightCommand := cmd.String("playwright-command")
	// pa11yConfigPath := cmd.String("config")
	// pa11yResultsPath := cmd.String("results")
	// noThresholdFail := cmd.Bool("no-threshold-fail")

	d, err := dagger.Connect(ctx)
	if err != nil {
		return fmt.Errorf("failed to connect to Dagger: %w", err)
	}

	// Explicitly only the files used by the grafana-server service
	grafanaHostSrc := d.Host().Directory(grafanaDir, dagger.HostDirectoryOpts{
		Include: []string{
			"./devenv",

			// Must build test plugins to run e2e tests
			"./e2e-playwright/test-plugins",
			"./packages/grafana-plugin-configs",

			"./scripts/grafana-server/custom.ini",
			"./scripts/grafana-server/start-server",
			"./scripts/grafana-server/kill-server",
			"./scripts/grafana-server/variables",
		},
	})

	// Minimal files needed to run yarn install
	yarnHostSrc := d.Host().Directory(grafanaDir, dagger.HostDirectoryOpts{
		Include: []string{
			"package.json",
			"yarn.lock",
			".yarnrc.yml",
			".yarn",
			"packages/*/package.json",
			"packages/grafana-data",
			"packages/grafana-i18n",
			"packages/grafana-runtime",
			"packages/grafana-schema",
			"packages/grafana-ui",
			"packages/grafana-plugin-configs",
			"public/app/plugins/*/*/package.json",
			"e2e-playwright/test-plugins/*/package.json",
			".nvmrc",
		},
	})

	// Files needed to run e2e tests. yarnHostSrc will be copied into the test runner container as well.
	e2eHostSrc := d.Host().Directory(".", dagger.HostDirectoryOpts{
		Include: []string{
			"public/app/types/*.d.ts",
			"public/app/core/icons/cached.json",

			// packages we use in playwright tests
			"packages",
			"public",

			// e2e files
			"e2e-playwright",
			"e2e-playwright/test-plugins",
			"playwright.config.ts",
		},
		Exclude: []string{
			"**/dist",
		},
	})

	yarnCache := d.CacheVolume("yarn-cache")
	frontendContainer, err := e2eutil.WithFrontendContainer(ctx, d, yarnHostSrc, yarnCache)
	if err != nil {
		return fmt.Errorf("failed to create frontend container: %w", err)
	}

	targz := d.Host().File(targzPath)

	var license *dagger.File
	if licensePath != "" {
		license = d.Host().File(licensePath)
	}

	var cloudPluginCreds *dagger.File
	if cloudPluginCredsPath != "" {
		cloudPluginCreds = d.Host().File(cloudPluginCredsPath)
	}

	svc, err := GrafanaService(ctx, d, GrafanaServiceOpts{
		HostSrc:           grafanaHostSrc,
		FrontendContainer: frontendContainer,
		GrafanaTarGz:      targz,
		License:           license,
	})
	if err != nil {
		return fmt.Errorf("failed to create Grafana service: %w", err)
	}

	runOpts := RunTestOpts{
		GrafanaService:       svc,
		FrontendContainer:    frontendContainer,
		HostSrc:              e2eHostSrc,
		Shard:                pwShard,
		TestResultsExportDir: resultsDir,
		HTMLReportExportDir:  htmlDir,
		BlobReportExportDir:  blobDir,
		PlaywrightCommand:    playwrightCommand,
		CloudPluginCreds:     cloudPluginCreds,
	}

	c, runErr := RunTest(ctx, d, runOpts)
	if runErr != nil {
		return fmt.Errorf("failed to run e2e test suite: %w", runErr)
	}

	c, syncErr := c.Sync(ctx)
	if syncErr != nil {
		return fmt.Errorf("failed to sync e2e test suite: %w", syncErr)
	}

	code, codeErr := c.ExitCode(ctx)
	if codeErr != nil {
		return fmt.Errorf("failed to get exit code of e2e test suite: %w", codeErr)
	}

	if code == 0 {
		log.Printf("e2e tests passed with exit code %d", code)
	} else {
		return fmt.Errorf("e2e tests failed with exit code %d", code)
	}

	log.Println("e2e tests completed successfully")
	return nil
}

func mustBeFile(arg string, emptyOk bool) func(string) error {
	return func(s string) error {
		if s == "" {
			if emptyOk {
				return nil
			}
			return cli.Exit(arg+" cannot be empty", 1)
		}
		stat, err := os.Stat(s)
		if err != nil {
			return cli.Exit(arg+" does not exist or cannot be read: "+s, 1)
		}
		if stat.IsDir() {
			return cli.Exit(arg+" must be a file, not a directory: "+s, 1)
		}
		return nil
	}
}

func mustBeDir(arg string, emptyOk bool, notExistOk bool) func(string) error {
	return func(s string) error {
		if s == "" {
			if emptyOk {
				return nil
			}
			return cli.Exit(arg+" cannot be empty", 1)
		}
		stat, err := os.Stat(s)
		if err != nil {
			if notExistOk {
				return nil
			}
			return cli.Exit(arg+" does not exist or cannot be read: "+s, 1)
		}
		if !stat.IsDir() {
			return cli.Exit(arg+" must be a directory: "+s, 1)
		}
		return nil
	}
}
