package errs_test

import (
	"errors"
	"net/http"
	"testing"

	errs "gateway/internal/library/errors"
	"gateway/internal/library/errors/code"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

var app = errs.For(code.Facade)

func TestNewValidation(t *testing.T) {
	t.Parallel()

	_, err := errs.New(code.Scope(100), code.InputInvalidFormat, 0, "bad scope")
	if !errors.Is(err, errs.ErrInvalidCode) {
		t.Fatalf("expected ErrInvalidCode, got %v", err)
	}

	_, err = errs.New(code.Facade, code.Category(1000), 0, "bad category")
	if !errors.Is(err, errs.ErrInvalidCode) {
		t.Fatalf("expected ErrInvalidCode, got %v", err)
	}
}

func TestBuilderCode(t *testing.T) {
	t.Parallel()

	e := app.ResNotFound("user", "42")
	if e.DisplayCode() != "10301000" {
		t.Fatalf("DisplayCode = %s, want 10301000", e.DisplayCode())
	}
	if e.HTTPStatus() != http.StatusNotFound {
		t.Fatalf("HTTPStatus = %d, want 404", e.HTTPStatus())
	}
}

func TestWithCauseImmutable(t *testing.T) {
	t.Parallel()

	base := app.DBError("db fail")
	wrapped := base.WithCause(errors.New("pq: connection refused"))

	if base.Unwrap() != nil {
		t.Fatal("WithCause must not mutate original error")
	}
	if wrapped.Unwrap() == nil {
		t.Fatal("wrapped error should have cause")
	}
}

func TestWithScope(t *testing.T) {
	t.Parallel()

	e := app.ResNotFound("x")
	moved, err := e.WithScope(code.LocalAPI)
	if err != nil {
		t.Fatal(err)
	}
	if moved.DisplayCode() != "11301000" {
		t.Fatalf("DisplayCode = %s, want 11301000", moved.DisplayCode())
	}
	if e.DisplayCode() != "10301000" {
		t.Fatal("WithScope must not mutate original error")
	}
}

func TestIsUsesFullCode(t *testing.T) {
	t.Parallel()

	a := app.ResNotFound("a")
	b := errs.For(code.LocalAPI).ResNotFound("b")

	if errors.Is(a, b) {
		t.Fatal("same category/detail but different scope must not match")
	}
	if !errors.Is(a, app.ResNotFound("c")) {
		t.Fatal("same full code should match via errors.Is")
	}
}

func TestFromCodeRoundTrip(t *testing.T) {
	t.Parallel()

	raw := uint32(10301123)
	e, err := errs.FromCode(raw)
	if err != nil {
		t.Fatal(err)
	}
	if e.Code() != raw {
		t.Fatalf("Code = %d, want %d", e.Code(), raw)
	}
}

func TestGRPCStatusBuiltin(t *testing.T) {
	t.Parallel()

	st := status.New(codes.NotFound, "missing")
	e, err := errs.FromGRPCError(st.Err(), code.Facade)
	if err != nil {
		t.Fatal(err)
	}
	if e.Category() != code.CatGRPC {
		t.Fatalf("Category = %d, want %d", e.Category(), code.CatGRPC)
	}
	if e.Detail() != code.Detail(codes.NotFound) {
		t.Fatalf("Detail = %d, want %d", e.Detail(), codes.NotFound)
	}
	if e.Scope() != code.Facade {
		t.Fatalf("Scope = %d, want %d", e.Scope(), code.Facade)
	}
}

func TestGRPCRoundTrip(t *testing.T) {
	t.Parallel()

	orig, err := app.InputInvalidFormat("email invalid").WithDetail(7)
	if err != nil {
		t.Fatal(err)
	}
	st := orig.GRPCStatus()

	back, err := errs.FromGRPCError(st.Err())
	if err != nil {
		t.Fatal(err)
	}
	if back.Code() != orig.Code() {
		t.Fatalf("Code = %d, want %d", back.Code(), orig.Code())
	}
	if back.Error() != orig.Error() {
		t.Fatalf("msg = %q, want %q", back.Error(), orig.Error())
	}
	if back.GRPCCode() != codes.InvalidArgument {
		t.Fatalf("GRPCCode = %s, want InvalidArgument", back.GRPCCode())
	}
}

func TestCatGRPCHTTPStatus(t *testing.T) {
	t.Parallel()

	e := errs.MustNew(code.Facade, code.CatGRPC, code.Detail(codes.NotFound), "missing")
	if e.HTTPStatus() != http.StatusNotFound {
		t.Fatalf("HTTPStatus = %d, want 404", e.HTTPStatus())
	}
}

func TestExtendedCategoriesHTTPStatus(t *testing.T) {
	t.Parallel()

	cases := []struct {
		name   string
		got    *errs.Error
		status int
	}{
		{"InputMissingRequired", app.InputMissingRequired("name"), http.StatusBadRequest},
		{"InputUnsupportedMedia", app.InputUnsupportedMedia("application/xml"), http.StatusUnsupportedMediaType},
		{"InputPayloadTooLarge", app.InputPayloadTooLarge(), http.StatusRequestEntityTooLarge},
		{"DBUnavailable", app.DBUnavailable(), http.StatusServiceUnavailable},
		{"ResPreconditionFailed", app.ResPreconditionFailed("etag mismatch"), http.StatusPreconditionFailed},
		{"ResLocked", app.ResLocked(), http.StatusLocked},
		{"AuthMethodNotAllowed", app.AuthMethodNotAllowed("POST"), http.StatusMethodNotAllowed},
		{"SysNotImplemented", app.SysNotImplemented(), http.StatusNotImplemented},
		{"SysClientTimeout", app.SysClientTimeout(), http.StatusRequestTimeout},
		{"SvcRateLimited", app.SvcRateLimited("payment provider"), http.StatusTooManyRequests},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			if tc.got.HTTPStatus() != tc.status {
				t.Fatalf("HTTPStatus() = %d, want %d (code %s)", tc.got.HTTPStatus(), tc.status, tc.got.DisplayCode())
			}
		})
	}
}

func TestBuilderCustomDetail(t *testing.T) {
	t.Parallel()

	e := app.Code(code.SysTimeout, 42, "downstream timeout")
	if e.Detail() != 42 {
		t.Fatalf("Detail = %d, want 42", e.Detail())
	}
}
