package issuetrackerconformance

import (
	"context"
	"encoding/json"
	"errors"
	"os"
	"reflect"
	"testing"

	"github.com/nexuscrm/shared/pkg/contract"
	sharederrors "github.com/nexuscrm/shared/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
)

type pointReaderFunc func(context.Context, string, string, *models.UserSession) (contract.Record, error)

func (f pointReaderFunc) Retrieve(ctx context.Context, objectName, recordID string, user *models.UserSession) (contract.Record, error) {
	return f(ctx, objectName, recordID, user)
}

type statusError struct {
	status int
}

func (e statusError) Error() string   { return "classified fixture condition" }
func (e statusError) HTTPStatus() int { return e.status }

type renderedResult struct {
	Outcome Outcome         `json:"outcome"`
	Fields  []renderedField `json:"fields"`
}

type renderedField struct {
	Name  string    `json:"name"`
	Kind  FieldKind `json:"kind"`
	Null  bool      `json:"null,omitempty"`
	Value any       `json:"value,omitempty"`
}

func TestPointReadGoldenBehaviorMatrix(t *testing.T) {
	expected := readGoldenResults(t)
	user := &models.UserSession{ID: "user-1", ProfileID: "profile-1"}
	typedRecord := contract.Record{
		"Subject":         "Non-CRM point read",
		"Archived":        true,
		"RetryCount":      int32(7),
		"StoryPoints":     json.Number("12.50"),
		"DueDate":         "2026-07-11",
		"UpdatedAt":       "2026-07-11T09:10:11Z",
		"ProjectId":       "PRJ-001",
		"OptionalValue":   nil,
		"UnselectedValue": "must not be projected",
		// SecretNote is intentionally absent, matching an FLS-filtered record.
	}

	tests := []struct {
		name   string
		reader PointReader
		ctx    func() context.Context
	}{
		{
			name: "typed-root-values-null-absent-and-fls-omission",
			reader: pointReaderFunc(func(_ context.Context, objectName, recordID string, gotUser *models.UserSession) (contract.Record, error) {
				if objectName != IssueObject || recordID != "ISS-001" || gotUser != user {
					t.Fatalf("retrieve = (%q, %q, %p), want (%q, ISS-001, %p)", objectName, recordID, gotUser, IssueObject, user)
				}
				return typedRecord, nil
			}),
		},
		{
			name: "missing-record",
			reader: pointReaderFunc(func(context.Context, string, string, *models.UserSession) (contract.Record, error) {
				return nil, sharederrors.NewNotFoundWireError("fixture message is not projected")
			}),
		},
		{
			name: "rls-denied-is-masked-as-missing",
			reader: pointReaderFunc(func(context.Context, string, string, *models.UserSession) (contract.Record, error) {
				return nil, statusError{status: 404}
			}),
		},
		{
			name: "object-read-denied-is-forbidden",
			reader: pointReaderFunc(func(context.Context, string, string, *models.UserSession) (contract.Record, error) {
				return nil, statusError{status: 403}
			}),
		},
		{
			name: "context-canceled",
			reader: pointReaderFunc(func(context.Context, string, string, *models.UserSession) (contract.Record, error) {
				t.Fatal("canceled request reached point reader")
				return nil, nil
			}),
			ctx: func() context.Context {
				ctx, cancel := context.WithCancel(context.Background())
				cancel()
				return ctx
			},
		},
	}

	selection := []FieldSelection{
		{Name: "UpdatedAt", Kind: FieldDateTime},
		{Name: "SecretNote", Kind: FieldString},
		{Name: "StoryPoints", Kind: FieldDecimal},
		{Name: "Subject", Kind: FieldString},
		{Name: "RetryCount", Kind: FieldInteger},
		{Name: "ProjectId", Kind: FieldReference},
		{Name: "OptionalValue", Kind: FieldString},
		{Name: "NeverStored", Kind: FieldString},
		{Name: "DueDate", Kind: FieldDate},
		{Name: "Archived", Kind: FieldBoolean},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			consumer, err := NewConsumer(tt.reader, CurrentInProcessErrorClassifier)
			if err != nil {
				t.Fatalf("NewConsumer: %v", err)
			}
			ctx := context.Background()
			if tt.ctx != nil {
				ctx = tt.ctx()
			}
			got, err := consumer.Read(ctx, Request{RecordID: "ISS-001", Fields: selection}, user)
			if err != nil {
				t.Fatalf("Read: %v", err)
			}
			assertGoldenResult(t, expected, tt.name, got)
		})
	}
}

func TestSelectionIsDeterministicAndRejectsConflictingKinds(t *testing.T) {
	reader := pointReaderFunc(func(context.Context, string, string, *models.UserSession) (contract.Record, error) {
		return contract.Record{"B": "second", "A": "first"}, nil
	})
	consumer, err := NewConsumer(reader, CurrentInProcessErrorClassifier)
	if err != nil {
		t.Fatalf("NewConsumer: %v", err)
	}
	got, err := consumer.Read(context.Background(), Request{RecordID: "ISS-002", Fields: []FieldSelection{
		{Name: "B", Kind: FieldString},
		{Name: "A", Kind: FieldString},
		{Name: "B", Kind: FieldString},
	}}, nil)
	if err != nil {
		t.Fatalf("Read: %v", err)
	}
	want := []RootField{{Name: "A", Kind: FieldString, Value: "first"}, {Name: "B", Kind: FieldString, Value: "second"}}
	if !reflect.DeepEqual(got.Fields, want) {
		t.Fatalf("fields = %#v, want %#v", got.Fields, want)
	}

	_, err = consumer.Read(context.Background(), Request{RecordID: "ISS-002", Fields: []FieldSelection{
		{Name: "A", Kind: FieldString},
		{Name: "A", Kind: FieldReference},
	}}, nil)
	if err == nil {
		t.Fatal("conflicting field kinds succeeded")
	}
}

func TestUnexpectedErrorsAndTypeMismatchesFailTheConformanceRun(t *testing.T) {
	tests := []struct {
		name   string
		reader PointReader
	}{
		{
			name: "unexpected error",
			reader: pointReaderFunc(func(context.Context, string, string, *models.UserSession) (contract.Record, error) {
				return nil, errors.New("infrastructure detail")
			}),
		},
		{
			name: "type mismatch",
			reader: pointReaderFunc(func(context.Context, string, string, *models.UserSession) (contract.Record, error) {
				return contract.Record{"Archived": "true"}, nil
			}),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			consumer, err := NewConsumer(tt.reader, CurrentInProcessErrorClassifier)
			if err != nil {
				t.Fatalf("NewConsumer: %v", err)
			}
			_, err = consumer.Read(context.Background(), Request{RecordID: "ISS-003", Fields: []FieldSelection{{Name: "Archived", Kind: FieldBoolean}}}, nil)
			if err == nil {
				t.Fatal("Read succeeded; want conformance failure")
			}
		})
	}
}

func readGoldenResults(t *testing.T) map[string]json.RawMessage {
	t.Helper()
	data, err := os.ReadFile("testdata/point_read_golden.json")
	if err != nil {
		t.Fatalf("read golden: %v", err)
	}
	var out map[string]json.RawMessage
	if err := json.Unmarshal(data, &out); err != nil {
		t.Fatalf("decode golden: %v", err)
	}
	return out
}

func assertGoldenResult(t *testing.T, expected map[string]json.RawMessage, name string, got Result) {
	t.Helper()
	rendered := renderedResult{Outcome: got.Outcome, Fields: make([]renderedField, 0, len(got.Fields))}
	for _, field := range got.Fields {
		rendered.Fields = append(rendered.Fields, renderedField{Name: field.Name, Kind: field.Kind, Null: field.Null, Value: field.Value})
	}
	gotJSON, err := json.Marshal(rendered)
	if err != nil {
		t.Fatalf("marshal result: %v", err)
	}
	wantJSON, ok := expected[name]
	if !ok {
		t.Fatalf("golden case %q is missing", name)
	}
	var gotValue, wantValue any
	if err := json.Unmarshal(gotJSON, &gotValue); err != nil {
		t.Fatalf("decode result: %v", err)
	}
	if err := json.Unmarshal(wantJSON, &wantValue); err != nil {
		t.Fatalf("decode expected: %v", err)
	}
	if !reflect.DeepEqual(gotValue, wantValue) {
		gotPretty, _ := json.MarshalIndent(gotValue, "", "  ")
		wantPretty, _ := json.MarshalIndent(wantValue, "", "  ")
		t.Fatalf("golden mismatch\ngot:\n%s\nwant:\n%s", gotPretty, wantPretty)
	}
}
