package auth_test

import (
	"context"
	"net"
	"net/http"
	"net/http/httptest"
	"regexp"
	"sync"
	"testing"
	"time"

	"golang.org/x/oauth2"

	"git.d-ma.be/mathias/tapir/internal/auth"
)

// fakeWriter is a TokenWriter capturing the persisted (ref, value).
type fakeWriter struct {
	mu       sync.Mutex
	ref, val string
	calls    int
}

func (w *fakeWriter) Put(ref, value string) error {
	w.mu.Lock()
	defer w.mu.Unlock()
	w.ref, w.val, w.calls = ref, value, w.calls+1
	return nil
}

// tokenServer fakes Google's token endpoint, returning the given JSON body for
// any POST. No live Google contact.
func tokenServer(t *testing.T, body string) *httptest.Server {
	t.Helper()
	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(body))
	}))
}

func cfg(srvURL string) auth.Config {
	return auth.Config{
		ClientID:     "cid",
		ClientSecret: "csecret",
		RedirectURL:  "http://localhost:18099/callback",
		TokenRef:     "youtube/refresh_token",
		Endpoint:     oauth2.Endpoint{AuthURL: srvURL + "/auth", TokenURL: srvURL + "/token"},
	}
}

func TestExchange_PersistsRefreshToken(t *testing.T) {
	srv := tokenServer(t, `{"access_token":"at","refresh_token":"rt-secret","token_type":"Bearer","expires_in":3600}`)
	defer srv.Close()

	w := &fakeWriter{}
	if err := auth.Exchange(context.Background(), cfg(srv.URL), w, "the-code"); err != nil {
		t.Fatalf("Exchange: %v", err)
	}
	if w.ref != "youtube/refresh_token" {
		t.Errorf("persisted ref = %q", w.ref)
	}
	if w.val != "rt-secret" {
		t.Errorf("persisted token = %q, want rt-secret", w.val)
	}
}

func TestExchange_RejectsMissingRefreshToken(t *testing.T) {
	srv := tokenServer(t, `{"access_token":"at","token_type":"Bearer","expires_in":3600}`)
	defer srv.Close()

	w := &fakeWriter{}
	err := auth.Exchange(context.Background(), cfg(srv.URL), w, "the-code")
	if err == nil {
		t.Fatal("want error when no refresh token returned")
	}
	if w.calls != 0 {
		t.Errorf("nothing should be persisted on failure; Put called %d times", w.calls)
	}
}

// stateRe pulls the CSRF state out of the printed consent URL.
var stateRe = regexp.MustCompile(`[?&]state=([a-f0-9]+)`)

// urlWriter forwards each Write to a channel so the test can read the consent
// URL Run prints before it blocks on the redirect.
type urlWriter struct{ ch chan string }

func (w urlWriter) Write(p []byte) (int, error) {
	w.ch <- string(p)
	return len(p), nil
}

func TestRun_FullFlow(t *testing.T) {
	srv := tokenServer(t, `{"access_token":"at","refresh_token":"rt-secret","token_type":"Bearer","expires_in":3600}`)
	defer srv.Close()

	// Ensure the fixed redirect port is free before binding.
	if ln, err := net.Listen("tcp", "localhost:18099"); err == nil {
		_ = ln.Close()
	} else {
		t.Skipf("redirect port 18099 unavailable: %v", err)
	}

	w := &fakeWriter{}
	out := urlWriter{ch: make(chan string, 4)}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	runErr := make(chan error, 1)
	go func() { runErr <- auth.Run(ctx, cfg(srv.URL), w, out) }()

	// First message carries the consent URL with the state param.
	var state string
	select {
	case msg := <-out.ch:
		m := stateRe.FindStringSubmatch(msg)
		if m == nil {
			t.Fatalf("no state in consent message: %q", msg)
		}
		state = m[1]
	case <-time.After(3 * time.Second):
		t.Fatal("timed out waiting for consent URL")
	}

	// Simulate the browser hitting the local redirect with code + state.
	resp, err := http.Get("http://localhost:18099/callback?state=" + state + "&code=the-code")
	if err != nil {
		t.Fatalf("callback GET: %v", err)
	}
	_ = resp.Body.Close()

	select {
	case err := <-runErr:
		if err != nil {
			t.Fatalf("Run: %v", err)
		}
	case <-time.After(3 * time.Second):
		t.Fatal("Run did not complete after callback")
	}

	if w.val != "rt-secret" {
		t.Errorf("persisted token = %q, want rt-secret", w.val)
	}
}

func TestRun_RejectsStateMismatch(t *testing.T) {
	srv := tokenServer(t, `{"access_token":"at","refresh_token":"rt","token_type":"Bearer","expires_in":3600}`)
	defer srv.Close()

	if ln, err := net.Listen("tcp", "localhost:18099"); err == nil {
		_ = ln.Close()
	} else {
		t.Skipf("redirect port 18099 unavailable: %v", err)
	}

	w := &fakeWriter{}
	out := urlWriter{ch: make(chan string, 4)}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	runErr := make(chan error, 1)
	go func() { runErr <- auth.Run(ctx, cfg(srv.URL), w, out) }()

	select {
	case <-out.ch: // drain consent URL
	case <-time.After(3 * time.Second):
		t.Fatal("timed out waiting for consent URL")
	}

	resp, err := http.Get("http://localhost:18099/callback?state=WRONG&code=the-code")
	if err != nil {
		t.Fatalf("callback GET: %v", err)
	}
	_ = resp.Body.Close()

	select {
	case err := <-runErr:
		if err == nil {
			t.Fatal("want error on state mismatch")
		}
	case <-time.After(3 * time.Second):
		t.Fatal("Run did not return after bad callback")
	}
	if w.calls != 0 {
		t.Errorf("no token should be persisted on state mismatch; Put called %d times", w.calls)
	}
}
