package apierror

import (
	"errors"
	"fmt"
	"testing"
)

func TestError_ErrorString_NoCause(t *testing.T) {
	e := &Error{Message: "x"}
	if got := e.Error(); got != "x" {
		t.Fatalf("Error() = %q, want %q", got, "x")
	}
}

func TestError_ErrorString_WithCause(t *testing.T) {
	e := &Error{Message: "x", Cause: errors.New("y")}
	if got := e.Error(); got != "x: y" {
		t.Fatalf("Error() = %q, want %q", got, "x: y")
	}
}

func TestError_Unwrap(t *testing.T) {
	cause := errors.New("boom")
	e := &Error{Message: "x", Cause: cause}
	if got := e.Unwrap(); got != cause {
		t.Fatalf("Unwrap() = %v, want %v", got, cause)
	}

	e2 := &Error{Message: "x"}
	if got := e2.Unwrap(); got != nil {
		t.Fatalf("Unwrap() with nil Cause = %v, want nil", got)
	}
}

func TestFrom_NilReturnsNil(t *testing.T) {
	if got := From(nil); got != nil {
		t.Fatalf("From(nil) = %v, want nil", got)
	}
}

func TestFrom_PreservesAPIError(t *testing.T) {
	orig := NotFound("track")
	got := From(orig)
	if got != orig {
		t.Fatalf("From(orig) returned different pointer: got %p, want %p", got, orig)
	}
}

func TestFrom_PreservesWrappedAPIError(t *testing.T) {
	inner := NotFound("track")
	wrapped := fmt.Errorf("ctx: %w", inner)
	got := From(wrapped)
	if got == nil {
		t.Fatal("From(wrapped) = nil, want *Error")
	}
	if got.Code != "track_not_found" {
		t.Fatalf("got.Code = %q, want %q", got.Code, "track_not_found")
	}
}

func TestFrom_WrapsPlainError(t *testing.T) {
	plain := errors.New("boom")
	got := From(plain)
	if got == nil {
		t.Fatal("From(plain) = nil, want *Error")
	}
	if got.Status != 500 {
		t.Errorf("Status = %d, want 500", got.Status)
	}
	if got.Code != "server_error" {
		t.Errorf("Code = %q, want %q", got.Code, "server_error")
	}
	if got.Cause != plain {
		t.Errorf("Cause = %v, want %v", got.Cause, plain)
	}
}

func TestConstructors_FieldsCorrect(t *testing.T) {
	tests := []struct {
		name        string
		err         *Error
		wantStatus  int
		wantCode    string
		wantMessage string
	}{
		{
			name:        "NotFound",
			err:         NotFound("track"),
			wantStatus:  404,
			wantCode:    "track_not_found",
			wantMessage: "track not found",
		},
		{
			name:        "BadRequest",
			err:         BadRequest("invalid_input", "invalid input"),
			wantStatus:  400,
			wantCode:    "invalid_input",
			wantMessage: "invalid input",
		},
		{
			name:        "Conflict",
			err:         Conflict("already_exists", "already exists"),
			wantStatus:  409,
			wantCode:    "already_exists",
			wantMessage: "already exists",
		},
		{
			name:        "Forbidden",
			err:         Forbidden("forbidden", "forbidden"),
			wantStatus:  403,
			wantCode:    "forbidden",
			wantMessage: "forbidden",
		},
		{
			name:        "Unauthorized",
			err:         Unauthorized("unauthorized", "authentication required"),
			wantStatus:  401,
			wantCode:    "unauthorized",
			wantMessage: "authentication required",
		},
		{
			name:        "Internal",
			err:         Internal(errors.New("boom")),
			wantStatus:  500,
			wantCode:    "server_error",
			wantMessage: "internal server error",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.err.Status != tt.wantStatus {
				t.Errorf("Status = %d, want %d", tt.err.Status, tt.wantStatus)
			}
			if tt.err.Code != tt.wantCode {
				t.Errorf("Code = %q, want %q", tt.err.Code, tt.wantCode)
			}
			if tt.err.Message != tt.wantMessage {
				t.Errorf("Message = %q, want %q", tt.err.Message, tt.wantMessage)
			}
		})
	}
}

func TestInternalMsg(t *testing.T) {
	cause := errors.New("db down")
	e := InternalMsg("lookup failed", cause)
	if e.Status != 500 {
		t.Errorf("Status = %d, want 500", e.Status)
	}
	if e.Code != "server_error" {
		t.Errorf("Code = %q, want server_error", e.Code)
	}
	if e.Message != "lookup failed" {
		t.Errorf("Message = %q, want lookup failed", e.Message)
	}
	if !errors.Is(e, cause) {
		t.Errorf("expected Is(cause) to be true")
	}
}

func TestSentinels_Identity(t *testing.T) {
	wrapped := fmt.Errorf("ctx: %w", ErrNotFound)

	var apiErr *Error
	if !errors.As(wrapped, &apiErr) {
		t.Fatal("errors.As(wrapped, &apiErr) = false, want true")
	}
	if apiErr != ErrNotFound {
		t.Fatalf("recovered error pointer = %p, want %p", apiErr, ErrNotFound)
	}
}
