package repository

import (
	"context"
	"fmt"
	"strings"
	"testing"
	"time"

	"apps/backend/internal/module/radar/domain"

	"go.mongodb.org/mongo-driver/bson"
)

func TestReviewStateListClauseLegacyPending(t *testing.T) {
	pending := fmt.Sprintf("%#v", reviewStateListClause(domain.ReviewPending))
	for _, part := range []string{domain.ReviewPending, "$nin", domain.OppAccepted, domain.OppDismissed} {
		if !strings.Contains(pending, part) {
			t.Fatalf("legacy pending clause missing %q: %s", part, pending)
		}
	}
	completed := fmt.Sprintf("%#v", reviewStateListClause(domain.ReviewCompleted))
	if !strings.Contains(completed, domain.ReviewCompleted) || !strings.Contains(completed, domain.OppAccepted) {
		t.Fatalf("completed clause: %s", completed)
	}
	removed := fmt.Sprintf("%#v", reviewStateListClause(domain.ReviewRemoved))
	if !strings.Contains(removed, domain.ReviewRemoved) || !strings.Contains(removed, domain.OppDismissed) {
		t.Fatalf("removed clause: %s", removed)
	}
}

func TestInboxTimeScopeClauseOldPostedCreatedTodayNoLastMatched(t *testing.T) {
	from, to := domain.LocalDayBounds(domain.NowNano(), domain.DisplayLocation())
	old := from - int64(48*time.Hour)
	clause := inboxTimeScopeClause(from, to)

	justFound := bson.M{"posted_at": old, "created_at": from + 1}
	if !bsonMMatches(clause, justFound) {
		t.Fatalf("posted=old created=today last_matched missing must match today $or; clause=%#v", clause)
	}
	zeroLast := bson.M{"posted_at": old, "created_at": from + 1, "last_matched_at": int64(0)}
	if !bsonMMatches(clause, zeroLast) {
		t.Fatalf("posted=old created=today last_matched=0 must match today $or; clause=%#v", clause)
	}
	staleMatch := bson.M{"posted_at": old, "created_at": from + 1, "last_matched_at": from - int64(24*time.Hour)}
	if bsonMMatches(clause, staleMatch) {
		t.Fatal("old last_matched must not fall back to created_at")
	}
}

func TestListOpportunitiesCreatedTodayOldPostedWithoutUpsert(t *testing.T) {
	ctx := context.Background()
	m := NewMemory()
	now := domain.NowNano()
	from, to := domain.LocalDayBounds(now, domain.DisplayLocation())
	row := sampleOpp("seed-no-upsert", []string{"漏水"}, 80)
	row.ID = "seed-no-upsert"
	row.PostedAt = from - int64(48*time.Hour)
	row.CreatedAt = from + 1
	row.LastMatchedAt = 0
	row.ReviewState = ""
	m.opportunities[row.ID] = row

	pending, total, err := m.ListOpportunities(ctx, 42, domain.OpportunityListFilter{
		ReviewState: domain.ReviewPending, TimeScope: "today", PostedFrom: from, PostedTo: to,
	})
	if err != nil || total != 1 || len(pending) != 1 || pending[0].ID != "seed-no-upsert" {
		t.Fatalf("no-backfill just-found row missing from pending today: total=%d list=%d err=%v", total, len(pending), err)
	}
}

func bsonMMatches(query bson.M, doc bson.M) bool {
	if or, ok := query["$or"]; ok && !bsonOrMatches(or, doc) {
		return false
	}
	if and, ok := query["$and"]; ok && !bsonAndMatches(and, doc) {
		return false
	}
	for key, cond := range query {
		if key == "$or" || key == "$and" {
			continue
		}
		if !bsonFieldMatches(key, cond, doc) {
			return false
		}
	}
	return true
}

func bsonOrMatches(raw any, doc bson.M) bool {
	items, ok := raw.(bson.A)
	if !ok {
		return false
	}
	for _, item := range items {
		clause, ok := item.(bson.M)
		if ok && bsonMMatches(clause, doc) {
			return true
		}
	}
	return false
}

func bsonAndMatches(raw any, doc bson.M) bool {
	items, ok := raw.(bson.A)
	if !ok {
		return false
	}
	for _, item := range items {
		clause, ok := item.(bson.M)
		if !ok || !bsonMMatches(clause, doc) {
			return false
		}
	}
	return true
}

func bsonFieldMatches(key string, cond any, doc bson.M) bool {
	got, exists := doc[key]
	switch c := cond.(type) {
	case bson.M:
		if flag, ok := c["$exists"]; ok {
			want, _ := flag.(bool)
			if exists != want {
				return false
			}
		}
		if gte, ok := c["$gte"]; ok {
			if !exists || toQueryInt64(got) < toQueryInt64(gte) {
				return false
			}
		}
		if lt, ok := c["$lt"]; ok {
			if !exists || toQueryInt64(got) >= toQueryInt64(lt) {
				return false
			}
		}
		return true
	default:
		return exists && toQueryInt64(got) == toQueryInt64(c)
	}
}

func toQueryInt64(v any) int64 {
	switch n := v.(type) {
	case int64:
		return n
	case int:
		return int64(n)
	case float64:
		return int64(n)
	default:
		return 0
	}
}
