package memory
import("encoding/json";"os";"path/filepath";"testing";"time";"fmt";"github.com/smolnikov-k/capyagent/core/internal/scope")
type researchDoc struct{ID string `json:"id"`;Text string `json:"text"`;Tags []string `json:"tags"`}
type researchQuery struct{ID string `json:"id"`;Category string `json:"category"`;Query string `json:"query"`;Relevant []string `json:"relevant"`}
func TestResearchMemoryRetrievalV1(t *testing.T){
 var fixture struct{Documents []researchDoc `json:"documents"`;Queries []researchQuery `json:"queries"`};data,err:=os.ReadFile(os.Getenv("RESEARCH_FIXTURES"));if err!=nil{t.Fatal(err)};if err=json.Unmarshal(data,&fixture);err!=nil{t.Fatal(err)}
 dir:=t.TempDir();s,err:=NewFile(dir);if err!=nil{t.Fatal(err)};s.now=func()time.Time{return time.Date(2026,11,1,0,0,0,0,time.UTC)}
 // Bulk seed the documented JSONL Note format, equal timestamps/tags, bypassing insertion semantics.
 f,err:=os.Create(filepath.Join(dir,"notes.jsonl"));if err!=nil{t.Fatal(err)};enc:=json.NewEncoder(f);for _,d:=range fixture.Documents{if err:=enc.Encode(Note{ID:d.ID,Text:d.Text,Tags:d.Tags,At:"2026-10-01T00:00:00Z"});err!=nil{t.Fatal(err)}};f.Close()
 rows:=[]map[string]any{};for _,q:=range fixture.Queries{start:=time.Now();notes,err:=s.Recall(q.Query,5);if err!=nil{t.Fatal(err)};ids:=[]string{};for _,n:=range notes{ids=append(ids,n.ID)};rows=append(rows,map[string]any{"id":q.ID,"category":q.Category,"ranked_ids":ids,"elapsed_ns":time.Since(start).Nanoseconds()})};out,_:=json.MarshalIndent(rows,"","  ");if err:=os.WriteFile(os.Getenv("RESEARCH_RESULTS"),out,0600);err!=nil{t.Fatal(err)}
}
func TestResearchScopeMatrixV2(t *testing.T){
 root:=t.TempDir();contexts:=[]scope.Context{};for tenant:=0;tenant<2;tenant++{for org:=0;org<2;org++{for team:=0;team<2;team++{for actor:=0;actor<2;actor++{for profile:=0;profile<2;profile++{contexts=append(contexts,scope.Context{TenantID:fmt.Sprint("tenant",tenant),OrganizationID:fmt.Sprint("org",org),TeamID:fmt.Sprint("team",team),ActorID:fmt.Sprint("actor",actor),ProfileID:fmt.Sprint("profile",profile),SessionID:"synthetic-session"})}}}}}
 // Independent fixture policy: personal memory is per tenant/org/actor/profile; team does not change its owner.
 sameOwner:=func(a,b scope.Context)bool{return a.TenantID==b.TenantID&&a.OrganizationID==b.OrganizationID&&a.ActorID==b.ActorID&&a.ProfileID==b.ProfileID}
 pooled,err:=NewFile(filepath.Join(root,"pooled-control"));if err!=nil{t.Fatal(err)};ids:=map[string]int{};pooledIDs:=map[string]int{}
 for i,c:=range contexts{s,err:=NewScopedFile(root,c,scope.Private);if err!=nil{t.Fatal(err)};text:=fmt.Sprintf("контрольная метка владельца %03d",i);n,err:=s.Remember(text,nil);if err!=nil{t.Fatal(err)};ids[n.ID]=i;pn,err:=pooled.Remember(text,nil);if err!=nil{t.Fatal(err)};pooledIDs[pn.ID]=i}
 rows:=[]map[string]any{};for i,c:=range contexts{s,err:=NewScopedFile(root,c,scope.Private);if err!=nil{t.Fatal(err)};got,err:=s.Recall("контрольная метка",100);if err!=nil{t.Fatal(err)};foreign,allowed,expected:=0,0,0;for _,owner:=range contexts{if sameOwner(c,owner){expected++}};for _,n:=range got{j,ok:=ids[n.ID];if !ok{t.Fatal("unmapped fixture note")};if sameOwner(c,contexts[j]){allowed++}else{foreign++}}
 control,err:=pooled.Recall("контрольная метка",100);if err!=nil{t.Fatal(err)};controlForeign:=0;for _,n:=range control{j,ok:=pooledIDs[n.ID];if !ok{t.Fatal("unmapped control note")};if !sameOwner(c,contexts[j]){controlForeign++}}
 rows=append(rows,map[string]any{"case":i,"expected_allowed_hits":expected,"allowed_hits":allowed,"forbidden_hits":foreign,"pooled_control_returned_hits":len(control),"pooled_control_forbidden_hits":controlForeign})};out,_:=json.MarshalIndent(rows,"","  ");if err:=os.WriteFile(os.Getenv("RESEARCH_SCOPE_RESULTS"),out,0600);err!=nil{t.Fatal(err)}
}
