package rule

import (
	"context"
	"testing"

	"github.com/sagernet/sing-box/adapter"
	C "github.com/sagernet/sing-box/constant"
	"github.com/sagernet/sing/common/x/list"

	"github.com/stretchr/testify/require"
	"go4.org/netipx"
)

type fakeRuleSet struct {
	matched bool
}

func (f *fakeRuleSet) Name() string {
	return "fake-rule-set"
}

func (f *fakeRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) error {
	return nil
}

func (f *fakeRuleSet) PostStart() error {
	return nil
}

func (f *fakeRuleSet) Metadata() adapter.RuleSetMetadata {
	return adapter.RuleSetMetadata{}
}

func (f *fakeRuleSet) ExtractIPSet() []*netipx.IPSet {
	return nil
}

func (f *fakeRuleSet) IncRef() {}

func (f *fakeRuleSet) DecRef() {}

func (f *fakeRuleSet) Cleanup() {}

func (f *fakeRuleSet) RegisterCallback(adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
	return nil
}

func (f *fakeRuleSet) UnregisterCallback(*list.Element[adapter.RuleSetUpdateCallback]) {}

func (f *fakeRuleSet) Close() error {
	return nil
}

func (f *fakeRuleSet) Match(*adapter.InboundContext) bool {
	return f.matched
}

func (f *fakeRuleSet) String() string {
	return "fake-rule-set"
}

type fakeRuleItem struct {
	matched bool
}

func (f *fakeRuleItem) Match(*adapter.InboundContext) bool {
	return f.matched
}

func (f *fakeRuleItem) String() string {
	return "fake-rule-item"
}

func newRuleSetOnlyRule(ruleSetMatched bool, invert bool) *DefaultRule {
	ruleSetItem := &RuleSetItem{
		setList: []adapter.RuleSet{&fakeRuleSet{matched: ruleSetMatched}},
	}
	return &DefaultRule{
		abstractDefaultRule: abstractDefaultRule{
			ruleSetItem: ruleSetItem,
			allItems:    []RuleItem{ruleSetItem},
			invert:      invert,
		},
	}
}

func newSingleItemRule(matched bool) *DefaultRule {
	item := &fakeRuleItem{matched: matched}
	return &DefaultRule{
		abstractDefaultRule: abstractDefaultRule{
			items:    []RuleItem{item},
			allItems: []RuleItem{item},
		},
	}
}

func TestAbstractDefaultRule_RuleSetOnly_InvertFalse(t *testing.T) {
	t.Parallel()
	require.True(t, newRuleSetOnlyRule(true, false).Match(&adapter.InboundContext{}))
	require.False(t, newRuleSetOnlyRule(false, false).Match(&adapter.InboundContext{}))
}

func TestAbstractDefaultRule_RuleSetOnly_InvertTrue(t *testing.T) {
	t.Parallel()
	require.False(t, newRuleSetOnlyRule(true, true).Match(&adapter.InboundContext{}))
	require.True(t, newRuleSetOnlyRule(false, true).Match(&adapter.InboundContext{}))
}

func TestAbstractLogicalRule_And_WithRuleSetInvert(t *testing.T) {
	t.Parallel()
	testCases := []struct {
		name          string
		aMatched      bool
		ruleSetBMatch bool
		expected      bool
	}{
		{
			name:          "A true B true",
			aMatched:      true,
			ruleSetBMatch: true,
			expected:      false,
		},
		{
			name:          "A true B false",
			aMatched:      true,
			ruleSetBMatch: false,
			expected:      true,
		},
		{
			name:          "A false B true",
			aMatched:      false,
			ruleSetBMatch: true,
			expected:      false,
		},
		{
			name:          "A false B false",
			aMatched:      false,
			ruleSetBMatch: false,
			expected:      false,
		},
	}
	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()
			logicalRule := &abstractLogicalRule{
				mode: C.LogicalTypeAnd,
				rules: []adapter.HeadlessRule{
					newSingleItemRule(testCase.aMatched),
					newRuleSetOnlyRule(testCase.ruleSetBMatch, true),
				},
			}
			require.Equal(t, testCase.expected, logicalRule.Match(&adapter.InboundContext{}))
		})
	}
}
