package state

import (
	"context"
	"database/sql"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/go-chi/chi/v5"
	"tangled.org/core/appview/config"
	"tangled.org/core/appview/models"
)

// routerFixture builds a minimal chi router wired to newDispatchHandler with
// spy handlers. userPath and stdPath are set to the URL.Path received by
// the respective spy after ServeHTTP returns.
func routerFixture(cfg *config.Config, repoDB fakeRepoDB) (router http.Handler, userPath *string, stdPath *string) {
	up := ""
	sp := ""
	userRouter := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		up = r.URL.Path
		w.WriteHeader(http.StatusOK)
	})
	stdRouter := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		sp = r.URL.Path
		w.WriteHeader(http.StatusOK)
	})

	r := chi.NewRouter()
	r.HandleFunc("/*", newDispatchHandler(cfg, repoDB, slog.Default(), userRouter, stdRouter))
	return r, &up, &sp
}

// fakeRepoDB implements db.Execer. The nil value is safe to use for test
// cases that don't exercise the DID-resolution path.
type fakeRepoDB map[string]*models.Repo

func (f fakeRepoDB) Exec(query string, args ...any) (sql.Result, error) { return nil, nil }
func (f fakeRepoDB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
	return nil, nil
}
func (f fakeRepoDB) Query(query string, args ...any) (*sql.Rows, error) { return nil, nil }
func (f fakeRepoDB) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
	return nil, nil
}
func (f fakeRepoDB) QueryRow(query string, args ...any) *sql.Row { return nil }
func (f fakeRepoDB) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row {
	return nil
}
func (f fakeRepoDB) Prepare(query string) (*sql.Stmt, error) { return nil, nil }
func (f fakeRepoDB) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) {
	return nil, nil
}

// normalCfg returns a config with project mode disabled.
func normalCfg() *config.Config {
	return &config.Config{}
}

// projectCfg returns a config with project mode enabled for the given user.
func projectCfg(user string) *config.Config {
	return &config.Config{
		Project: config.ProjectConfig{
			Enabled: true,
			User:    user,
		},
	}
}

func TestDispatch_NormalMode(t *testing.T) {
	cases := []struct {
		name     string
		path     string
		wantUser string // expected URL seen by userRouter; empty means standardRouter should be hit
		wantStd  string // expected URL seen by standardRouter; empty means userRouter should be hit
		wantCode int    // expected HTTP status; 0 means 200
	}{
		{"handle", "/user.com/my-repo", "/user.com/my-repo", "", 0},
		{"handle root", "/user.com", "/user.com", "", 0},
		{"@handle redirect", "/@user.com/repo", "", "", http.StatusFound},
		{"flattened did redirect", "/did-plc-abc123xyz", "", "", http.StatusFound},
		{"settings", "/settings/profile", "", "/settings/profile", 0},
		{"login", "/login", "", "/login", 0},
		{"notifications", "/notifications", "", "/notifications", 0},
		{"signup", "/signup/complete", "", "/signup/complete", 0},
		{"root", "/", "", "/", 0},
		{"unknown segment", "/not-a-handle", "", "/not-a-handle", 0},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			router, userPath, stdPath := routerFixture(normalCfg(), nil)
			req := httptest.NewRequest(http.MethodGet, tc.path, nil)
			rr := httptest.NewRecorder()
			router.ServeHTTP(rr, req)

			wantCode := tc.wantCode
			if wantCode == 0 {
				wantCode = http.StatusOK
			}
			if rr.Code != wantCode {
				t.Errorf("status = %d, want %d", rr.Code, wantCode)
			}
			if tc.wantUser != "" && *userPath != tc.wantUser {
				t.Errorf("userRouter received %q, want %q", *userPath, tc.wantUser)
			}
			if tc.wantStd != "" && *stdPath != tc.wantStd {
				t.Errorf("stdRouter received %q, want %q", *stdPath, tc.wantStd)
			}
			if tc.wantCode == http.StatusFound {
				// neither spy should have been called
				if *userPath != "" || *stdPath != "" {
					t.Errorf("redirect case should not reach routers (user=%q std=%q)", *userPath, *stdPath)
				}
			}
		})
	}
}

func TestDispatch_ProjectMode(t *testing.T) {
	const projectUser = "anirudh.fi"

	cases := []struct {
		name     string
		path     string
		wantUser string
		wantStd  string
		wantCode int
	}{
		{"root becomes profile", "/", "/anirudh.fi", "", 0},
		{"repo", "/my-repo", "/anirudh.fi/my-repo", "", 0},
		{"repo with subpath", "/my-repo/issues/1", "/anirudh.fi/my-repo/issues/1", "", 0},
		{"settings prefix", "/settings/profile", "", "/settings/profile", 0},
		{"login prefix", "/login", "", "/login", 0},
		{"notifications prefix", "/notifications", "", "/notifications", 0},
		{"signup is standard prefix", "/signup", "", "/signup", 0},
		{"search prefix", "/search", "", "/search", 0},
		{"explicit handle still works", "/anirudh.fi/my-repo", "/anirudh.fi/my-repo", "", 0},
		{"other user still works", "/other.user/their-repo", "/other.user/their-repo", "", 0},
		{"@handle redirect", "/@anirudh.fi/repo", "", "", http.StatusFound},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			router, userPath, stdPath := routerFixture(projectCfg(projectUser), nil)
			req := httptest.NewRequest(http.MethodGet, tc.path, nil)
			rr := httptest.NewRecorder()
			router.ServeHTTP(rr, req)

			wantCode := tc.wantCode
			if wantCode == 0 {
				wantCode = http.StatusOK
			}
			if rr.Code != wantCode {
				t.Errorf("status = %d, want %d", rr.Code, wantCode)
			}
			if tc.wantUser != "" && *userPath != tc.wantUser {
				t.Errorf("userRouter received %q, want %q", *userPath, tc.wantUser)
			}
			if tc.wantStd != "" && *stdPath != tc.wantStd {
				t.Errorf("stdRouter received %q, want %q", *stdPath, tc.wantStd)
			}
		})
	}
}
