package web

import (
	"net/http/httptest"
	"testing"
	"time"

	"golang.org/x/oauth2"
)

func TestSignVerifyRoundTrip(t *testing.T) {
	a := &Authenticator{secret: []byte("test-secret-key-at-least-16")}
	payload := []byte(`{"sub":"abc","exp":123}`)
	tok := a.sign(payload)

	got, ok := a.verify(tok)
	if !ok {
		t.Fatal("verify rejected a freshly signed token")
	}
	if string(got) != string(payload) {
		t.Fatalf("payload mismatch: got %q want %q", got, payload)
	}
}

func TestVerifyRejectsTamperAndForeignKey(t *testing.T) {
	a := &Authenticator{secret: []byte("test-secret-key-at-least-16")}
	tok := a.sign([]byte(`{"sub":"abc"}`))

	// Flip a byte in the payload segment.
	bad := []byte(tok)
	bad[0] ^= 0x01
	if _, ok := a.verify(string(bad)); ok {
		t.Error("verify accepted a tampered token")
	}

	// A different key must not validate the same token.
	other := &Authenticator{secret: []byte("a-totally-different-key-16")}
	if _, ok := other.verify(tok); ok {
		t.Error("verify accepted a token signed with a different key")
	}

	if _, ok := a.verify("not-a-valid-token"); ok {
		t.Error("verify accepted a malformed token")
	}
}

func TestSafeNext(t *testing.T) {
	cases := map[string]string{
		"":                 "/",
		"/tech":            "/tech",
		"/tech?page=2":     "/tech?page=2",
		"//evil.com":       "/", // protocol-relative open redirect
		"https://evil.com": "/", // absolute URL
		"javascript:alert": "/", // not a path
	}
	for in, want := range cases {
		if got := safeNext(in); got != want {
			t.Errorf("safeNext(%q) = %q, want %q", in, got, want)
		}
	}
}

func TestMatrixUser(t *testing.T) {
	cases := []struct {
		name string
		u    SessionUser
		want string
	}{
		{"lowercased", SessionUser{Username: "Reala"}, "@reala:parodia.dev"},
		{"trimmed", SessionUser{Username: "  reala  "}, "@reala:parodia.dev"},
		{"old session with no username", SessionUser{Sub: "abc", Name: "Reala"}, ""},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := tc.u.MatrixUser("parodia.dev"); got != tc.want {
				t.Fatalf("MatrixUser = %q, want %q", got, tc.want)
			}
		})
	}
	if got := (&SessionUser{Username: "reala"}).MatrixUser(""); got != "" {
		t.Fatalf("no server name should yield no identity, got %q", got)
	}
}

func TestHostInDomain(t *testing.T) {
	cases := []struct {
		host, domain string
		want         bool
	}{
		{"games.parodia.dev", ".parodia.dev", true},
		{"news.parodia.dev:8080", ".parodia.dev", true},
		{"parodia.dev", ".parodia.dev", true},
		{"GAMES.PARODIA.DEV", ".parodia.dev", true},
		{"evil.com", ".parodia.dev", false},
		// The suffix check must not match a domain that merely ends in the
		// same letters: notparodia.dev is a different site entirely.
		{"notparodia.dev", ".parodia.dev", false},
		{"games.parodia.dev.evil.com", ".parodia.dev", false},
	}
	for _, tc := range cases {
		if got := hostInDomain(tc.host, tc.domain); got != tc.want {
			t.Errorf("hostInDomain(%q, %q) = %v, want %v", tc.host, tc.domain, got, tc.want)
		}
	}
}

func TestOAuthForKeepsLoginOnTheHostItStartedOn(t *testing.T) {
	base := &oauth2.Config{RedirectURL: "https://news.parodia.dev/auth/callback"}
	a := &Authenticator{oauth: base, domain: ".parodia.dev"}

	req := httptest.NewRequest("GET", "/auth/login", nil)
	req.Host = "games.parodia.dev"
	if got := a.oauthFor(req).RedirectURL; got != "https://games.parodia.dev/auth/callback" {
		t.Fatalf("games login should come back to games, got %q", got)
	}

	req.Host = "news.parodia.dev"
	if got := a.oauthFor(req).RedirectURL; got != base.RedirectURL {
		t.Fatalf("news login should use the configured URL, got %q", got)
	}

	// A Host we don't own must never be echoed back into a redirect URI.
	req.Host = "evil.com"
	if got := a.oauthFor(req).RedirectURL; got != base.RedirectURL {
		t.Fatalf("foreign host must fall back to the configured URL, got %q", got)
	}

	// With no cookie domain configured there is nothing to share, so the
	// configured redirect stands whatever the Host header says.
	off := &Authenticator{oauth: base}
	req.Host = "games.parodia.dev"
	if got := off.oauthFor(req).RedirectURL; got != base.RedirectURL {
		t.Fatalf("host-only mode must not rewrite the redirect, got %q", got)
	}
}

func TestSessionCookieIsSharedButOAuthCookieIsNot(t *testing.T) {
	a := &Authenticator{secret: []byte("test-secret-key-at-least-16"), domain: ".parodia.dev"}
	rec := httptest.NewRecorder()
	a.setCookie(rec, sessionCookie, "v", time.Minute)
	a.setCookie(rec, oauthCookie, "v", time.Minute)

	got := map[string]string{}
	for _, c := range rec.Result().Cookies() {
		got[c.Name] = c.Domain
	}
	if got[sessionCookie] != "parodia.dev" {
		t.Fatalf("session cookie domain = %q, want it shared across parodia.dev", got[sessionCookie])
	}
	if got[oauthCookie] != "" {
		t.Fatalf("oauth cookie must stay host-only, got domain %q", got[oauthCookie])
	}
}
