package vkid

import (
	"context"
	"encoding/base64"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"
)

// testExchanger builds an Exchanger pointed at a test server.
func testExchanger(endpoint string) *Exchanger {
	return &Exchanger{
		appID:        "app-1",
		clientSecret: "secret-1",
		redirectURI:  "https://example.test/app/",
		endpoint:     endpoint,
		httpClient:   &http.Client{},
	}
}

func TestExchangeSuccessSendsConfidentialPKCE(t *testing.T) {
	var gotForm url.Values
	var gotContentType string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		gotContentType = r.Header.Get("Content-Type")
		body, _ := io.ReadAll(r.Body)
		gotForm, _ = url.ParseQuery(string(body))
		w.Header().Set("Content-Type", "application/json")
		_, _ = io.WriteString(w, `{"user_id":"12345","access_token":"a","id_token":"h.e.s"}`)
	}))
	defer srv.Close()

	id, err := testExchanger(srv.URL).Exchange(context.Background(), "the-code", "dev-9", "verifier-xyz")
	if err != nil {
		t.Fatalf("Exchange: %v", err)
	}
	if id.ExternalID != "12345" {
		t.Fatalf("ExternalID = %q, want 12345", id.ExternalID)
	}
	if gotContentType != "application/x-www-form-urlencoded" {
		t.Errorf("Content-Type = %q", gotContentType)
	}
	// The confidential exchange must carry the PKCE inputs and the app credentials.
	want := map[string]string{
		"grant_type":    "authorization_code",
		"code":          "the-code",
		"code_verifier": "verifier-xyz",
		"device_id":     "dev-9",
		"client_id":     "app-1",
		"client_secret": "secret-1",
		"redirect_uri":  "https://example.test/app/",
	}
	for k, v := range want {
		if gotForm.Get(k) != v {
			t.Errorf("form[%s] = %q, want %q", k, gotForm.Get(k), v)
		}
	}
}

func TestExchangeAcceptsNumericUserID(t *testing.T) {
	// VK returns user_id as a bare JSON number in some responses; it must parse too.
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = io.WriteString(w, `{"user_id":1234567890,"access_token":"a"}`)
	}))
	defer srv.Close()

	id, err := testExchanger(srv.URL).Exchange(context.Background(), "c", "d", "v")
	if err != nil {
		t.Fatalf("Exchange: %v", err)
	}
	if id.ExternalID != "1234567890" {
		t.Fatalf("ExternalID = %q, want 1234567890", id.ExternalID)
	}
}

func TestExchangeFallsBackToIDTokenSub(t *testing.T) {
	sub := "987654321"
	payload := base64.RawURLEncoding.EncodeToString([]byte(`{"sub":"` + sub + `"}`))
	idToken := "header." + payload + ".sig"
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = io.WriteString(w, `{"access_token":"a","id_token":"`+idToken+`"}`)
	}))
	defer srv.Close()

	id, err := testExchanger(srv.URL).Exchange(context.Background(), "c", "d", "v")
	if err != nil {
		t.Fatalf("Exchange: %v", err)
	}
	if id.ExternalID != sub {
		t.Fatalf("ExternalID = %q, want %q (from id_token sub)", id.ExternalID, sub)
	}
}

func TestExchangeRejectedIsErrInvalid(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusBadRequest)
		_, _ = io.WriteString(w, `{"error":"invalid_grant","error_description":"code expired"}`)
	}))
	defer srv.Close()

	_, err := testExchanger(srv.URL).Exchange(context.Background(), "c", "d", "v")
	if !errors.Is(err, ErrInvalid) {
		t.Fatalf("err = %v, want ErrInvalid", err)
	}
}

func TestExchangeEmptyInputsFailFast(t *testing.T) {
	// Missing PKCE inputs are rejected without any network call.
	ex := testExchanger("http://127.0.0.1:0/never")
	for _, args := range [][3]string{{"", "d", "v"}, {"c", "", "v"}, {"c", "d", ""}} {
		if _, err := ex.Exchange(context.Background(), args[0], args[1], args[2]); !errors.Is(err, ErrInvalid) {
			t.Errorf("Exchange%v err = %v, want ErrInvalid", args, err)
		}
	}
}

func TestExchangeTransportErrorIsNotErrInvalid(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
	srv.Close() // closed listener → connection refused
	_, err := testExchanger(srv.URL).Exchange(context.Background(), "c", "d", "v")
	if err == nil {
		t.Fatal("want a transport error")
	}
	if errors.Is(err, ErrInvalid) {
		t.Fatalf("transport error must not be ErrInvalid: %v", err)
	}
}
