# Feature 004: Assignments & Scope Tags - Developer Quickstart

## Overview
This guide helps developers quickly set up their environment and start working on the Assignments & Scope Tags feature.

---

## Prerequisites

- **Laravel Sail** installed and running
- **PostgreSQL** database via Sail
- **Microsoft Graph API** test tenant credentials
- **Redis** (optional, for caching - Sail includes it)

---

## Quick Setup

### 1. Start Sail Environment

```bash
# Start all containers
./vendor/bin/sail up -d

# Check status
./vendor/bin/sail ps
```

### 2. Run Migrations

```bash
# Run new migrations for assignments
./vendor/bin/sail artisan migrate

# Verify new columns exist
./vendor/bin/sail artisan tinker
>>> Schema::hasColumn('backup_items', 'assignments')
=> true
>>> Schema::hasColumn('restore_runs', 'group_mapping')
=> true
```

### 3. Seed Test Data (Optional)

```bash
# Seed tenants, policies, and backup sets
./vendor/bin/sail artisan db:seed

# Or create specific test data
./vendor/bin/sail artisan tinker
>>> $tenant = Tenant::factory()->create(['name' => 'Test Tenant']);
>>> $policy = Policy::factory()->for($tenant)->create(['type' => 'settingsCatalogPolicy']);
>>> $backupSet = BackupSet::factory()->for($tenant)->create();
```

### 4. Configure Graph API Credentials

```bash
# Copy example env
cp .env.example .env.testing

# Add Graph API credentials
GRAPH_CLIENT_ID=your-client-id
GRAPH_CLIENT_SECRET=your-client-secret
GRAPH_TENANT_ID=your-test-tenant-id
```

---

## Development Workflow

### Running Tests

```bash
# Run all tests
./vendor/bin/sail artisan test

# Run specific test file
./vendor/bin/sail artisan test tests/Feature/BackupWithAssignmentsTest.php

# Run tests with filter
./vendor/bin/sail artisan test --filter=assignment

# Run tests with coverage
./vendor/bin/sail artisan test --coverage
```

### Using Tinker for Debugging

```bash
./vendor/bin/sail artisan tinker
```

**Common Tinker Commands**:

```php
// Fetch assignments for a policy
$fetcher = app(AssignmentFetcher::class);
$assignments = $fetcher->fetch('tenant-id', 'policy-graph-id');
dump($assignments);

// Test group resolution
$resolver = app(GroupResolver::class);
$groups = $resolver->resolveGroupIds(['group-1', 'group-2'], 'tenant-id');
dump($groups);

// Test backup with assignments
$service = app(AssignmentBackupService::class);
$backupItem = $service->backup(
    tenantId: 1,
    policyId: 'abc-123',
    includeAssignments: true
);
dump($backupItem->assignments);

// Test group mapping
$restoreRun = RestoreRun::first();
$restoreRun->addGroupMapping('source-group-1', 'target-group-1');
$restoreRun->save();
dump($restoreRun->group_mapping);

// Clear cache
Cache::flush();
```

---

## Manual Testing Scenarios

### Scenario 1: Backup with Assignments (Happy Path)

**Goal**: Verify assignments are captured during backup

**Steps**:
1. Navigate to Backup creation form: `/tenants/{tenant}/backups/create`
2. Select Settings Catalog policies
3. ✅ Check "Include Assignments & Scope Tags"
4. Click "Create Backup"
5. Wait for backup job to complete

**Verification**:
```bash
./vendor/bin/sail artisan tinker
>>> $backupItem = BackupItem::latest()->first();
>>> dump($backupItem->assignments);
>>> dump($backupItem->metadata['assignment_count']);
```

**Expected**:
- `assignments` column populated with JSON array
- `metadata['assignment_count']` matches actual count
- Audit log entry: `backup.assignments.included`

---

### Scenario 2: Policy View with Assignments Tab

**Goal**: Verify assignments display correctly in UI

**Steps**:
1. Navigate to Policy view: `/policies/{policy}`
2. Click "Assignments" tab

**Verification**:
- Table shows assignments (Type, Name, Mode, ID)
- Orphaned IDs render as "Unknown Group (ID: {id})" with warning icon
- Scope Tags section shows tag names + IDs
- Empty state if no assignments

**Edge Cases**:
- Policy with 0 assignments → Empty state
- Policy with orphaned group ID → Warning icon
- Policy without assignments metadata → Empty state

---

### Scenario 3: Restore with Group Mapping (Cross-Tenant)

**Goal**: Verify group mapping wizard works for cross-tenant restore

**Setup**:
```bash
# Create source and target tenants
./vendor/bin/sail artisan tinker
>>> $sourceTenant = Tenant::factory()->create(['name' => 'Source Tenant']);
>>> $targetTenant = Tenant::factory()->create(['name' => 'Target Tenant']);

# Create groups in both tenants (simulate via test data)
>>> Group::factory()->for($sourceTenant)->create(['graph_id' => 'source-group-1', 'display_name' => 'HR Team']);
>>> Group::factory()->for($targetTenant)->create(['graph_id' => 'target-group-1', 'display_name' => 'HR Department']);
```

**Steps**:
1. Navigate to Restore wizard: `/backups/{backup}/restore`
2. Select target tenant (different from source)
3. Click "Continue" to Restore Preview
4. **Group Mapping step should appear**:
   - Source group: "HR Team (source-group-1)"
   - Target group dropdown: searchable, populated with target tenant groups
   - Select "HR Department" (target-group-1)
5. Click "Continue"
6. Review Restore Preview (should show mapped group)
7. Click "Restore"

**Verification**:
```bash
./vendor/bin/sail artisan tinker
>>> $restoreRun = RestoreRun::latest()->first();
>>> dump($restoreRun->group_mapping);
=> ["source-group-1" => "target-group-1"]
```

**Expected**:
- Group mapping step only appears when unresolved groups detected
- Dropdown searchable with 500+ groups
- Mapping persisted in `restore_runs.group_mapping`
- Audit log entries: `restore.group_mapping.applied`

---

### Scenario 4: Handle Graph API Failures

**Goal**: Verify fail-soft behavior when Graph API fails

**Mock Graph Failure**:
```php
// In tests or local dev with Http::fake()
Http::fake([
    '*/assignments' => Http::response(null, 500), // Simulate failure
]);
```

**Steps**:
1. Create backup with "Include Assignments" checkbox
2. Observe behavior

**Verification**:
```bash
./vendor/bin/sail artisan tinker
>>> $backupItem = BackupItem::latest()->first();
>>> dump($backupItem->metadata['assignments_fetch_failed']);
=> true
>>> dump($backupItem->assignments);
=> null
```

**Expected**:
- Backup completes successfully (fail-soft)
- `assignments_fetch_failed` flag set to `true`
- Warning logged in `storage/logs/laravel.log`
- Audit log entry: `backup.assignments.fetch_failed`

---

### Scenario 5: Skip Assignments in Group Mapping

**Goal**: Verify "Skip" functionality in group mapping

**Steps**:
1. Follow Scenario 3 setup
2. In Group Mapping step:
   - Check "Skip" checkbox for one source group
   - Map other groups normally
3. Complete restore

**Verification**:
```bash
./vendor/bin/sail artisan tinker
>>> $restoreRun = RestoreRun::latest()->first();
>>> dump($restoreRun->group_mapping);
=> ["source-group-1" => "target-group-1", "source-group-2" => "SKIP"]

>>> dump($restoreRun->getSkippedAssignmentsCount());
=> 2 # Assignments targeting source-group-2 were skipped
```

**Expected**:
- Skipped group has `"SKIP"` value in mapping JSON
- Restore runs without creating assignments for skipped groups
- Audit log entries: `restore.assignment.skipped` (per skipped)

---

## Common Issues & Solutions

### Issue 1: Migrations Fail

**Error**: `SQLSTATE[42P01]: Undefined table: backup_items`

**Solution**:
```bash
# Reset database and re-run migrations
./vendor/bin/sail artisan migrate:fresh --seed
```

---

### Issue 2: Graph API Rate Limiting

**Error**: `429 Too Many Requests`

**Solution**:
```bash
# Add retry logic with exponential backoff (already in GraphClient)
# Or reduce test load:
# - Use Http::fake() in tests
# - Add delays between API calls (100ms)
```

**Check Rate Limit Headers**:
```php
// In GraphClient.php
Log::info('Graph API call', [
    'endpoint' => $endpoint,
    'retry_after' => $response->header('Retry-After'),
    'remaining_calls' => $response->header('X-RateLimit-Remaining'),
]);
```

---

### Issue 3: Cache Not Clearing

**Error**: Stale group names in UI after updating tenant

**Solution**:
```bash
# Clear all cache
./vendor/bin/sail artisan cache:clear

# Clear specific cache keys
./vendor/bin/sail artisan tinker
>>> Cache::forget('groups:tenant-id:*');
>>> Cache::forget('scope_tags:all');
```

---

### Issue 4: Assignments Not Showing in UI

**Checklist**:
1. ✅ Checkbox was enabled during backup?
2. ✅ `backup_items.assignments` column has data? (check via tinker)
3. ✅ Policy type is `settingsCatalogPolicy`? (others not supported in Phase 1)
4. ✅ Graph API call succeeded? (check logs for errors)

**Debug**:
```bash
./vendor/bin/sail artisan tinker
>>> $backupItem = BackupItem::find(123);
>>> dump($backupItem->assignments);
>>> dump($backupItem->hasAssignments());
>>> dump($backupItem->metadata['assignments_fetch_failed']);
```

---

### Issue 5: Group Mapping Dropdown Slow

**Symptom**: Dropdown takes 5+ seconds to load with 500+ groups

**Solutions**:
1. **Increase cache TTL**:
   ```php
   // In GroupResolver.php
   Cache::remember("groups:{$tenantId}", 600, function () { ... }); // 10 min
   ```

2. **Pre-warm cache**:
   ```php
   // In RestoreWizard.php mount()
   public function mount()
   {
       // Cache groups when wizard opens
       app(GroupResolver::class)->getAllForTenant($this->targetTenantId);
   }
   ```

3. **Add debouncing**:
   ```php
   // In Filament Select component
   ->debounce(500) // Wait 500ms after typing
   ```

---

## Performance Benchmarks

### Target Metrics

| Operation | Target | Actual (Measure) |
|-----------|--------|------------------|
| Assignment fetch (per policy) | < 2s | ___ |
| Group resolution (100 groups) | < 1s | ___ |
| Group mapping UI search | < 500ms | ___ |
| Assignment restore (20 policies) | < 30s | ___ |

### Measuring Performance

```bash
# Enable query logging
./vendor/bin/sail artisan tinker
>>> DB::enableQueryLog();

# Run operation
>>> $fetcher = app(AssignmentFetcher::class);
>>> $start = microtime(true);
>>> $assignments = $fetcher->fetch('tenant-id', 'policy-id');
>>> $duration = microtime(true) - $start;
>>> dump("Duration: {$duration}s");

# Check queries
>>> dump(DB::getQueryLog());
```

### Load Testing

```bash
# Create 100 policies with assignments
./vendor/bin/sail artisan tinker
>>> for ($i = 0; $i < 100; $i++) {
>>>     $policy = Policy::factory()->create(['type' => 'settingsCatalogPolicy']);
>>>     $backupItem = BackupItem::factory()->for($policy->backupSet)->create([
>>>         'assignments' => [/* 10 assignments */]
>>>     ]);
>>> }

# Measure restore time
>>> $start = microtime(true);
>>> $service = app(AssignmentRestoreService::class);
>>> $outcomes = $service->restoreBatch($policyIds, $groupMapping);
>>> $duration = microtime(true) - $start;
>>> dump("Restored 100 policies in {$duration}s");
```

---

## Debugging Tools

### 1. Laravel Telescope (Optional)

```bash
# Install Telescope (if not already)
./vendor/bin/sail composer require laravel/telescope --dev
./vendor/bin/sail artisan telescope:install
./vendor/bin/sail artisan migrate

# Access Telescope
# Navigate to: http://localhost/telescope
```

**Use Cases**:
- Monitor Graph API calls (Requests tab)
- Track slow queries (Queries tab)
- View cache hits/misses (Cache tab)
- Inspect jobs (Jobs tab)

---

### 2. Laravel Debugbar (Installed)

```bash
# Ensure Debugbar is enabled
DEBUGBAR_ENABLED=true # in .env
```

**Features**:
- Query count and duration per page load
- View all HTTP requests (including Graph API)
- Cache operations
- Timeline of execution

---

### 3. Graph API Explorer

**URL**: https://developer.microsoft.com/en-us/graph/graph-explorer

**Use Cases**:
- Test Graph API endpoints manually
- Verify assignment response structure
- Debug authentication issues
- Check available permissions

**Example Queries**:
```
GET /deviceManagement/configurationPolicies/{id}/assignments
POST /directoryObjects/getByIds
GET /deviceManagement/roleScopeTags
```

---

## Test Data Factories

### Factory: BackupItem with Assignments

```php
// database/factories/BackupItemFactory.php
public function withAssignments(int $count = 5): static
{
    return $this->state(fn (array $attributes) => [
        'assignments' => collect(range(1, $count))->map(fn ($i) => [
            'id' => "assignment-{$i}",
            'target' => [
                '@odata.type' => '#microsoft.graph.groupAssignmentTarget',
                'groupId' => "group-{$i}",
            ],
            'intent' => 'apply',
        ])->toArray(),
        'metadata' => array_merge($attributes['metadata'] ?? [], [
            'assignment_count' => $count,
            'scope_tag_ids' => ['0'],
            'scope_tag_names' => ['Default'],
        ]),
    ]);
}

// Usage
$backupItem = BackupItem::factory()->withAssignments(10)->create();
```

---

### Factory: RestoreRun with Group Mapping

```php
// database/factories/RestoreRunFactory.php
public function withGroupMapping(array $mapping = []): static
{
    return $this->state(fn (array $attributes) => [
        'group_mapping' => $mapping ?: [
            'source-group-1' => 'target-group-1',
            'source-group-2' => 'target-group-2',
        ],
    ]);
}

// Usage
$restoreRun = RestoreRun::factory()->withGroupMapping([
    'source-abc' => 'target-xyz',
])->create();
```

---

## CI/CD Integration

### GitHub Actions / Gitea CI (Example)

```yaml
# .gitea/workflows/test.yml
name: Test Feature 004

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Sail
        run: |
          docker-compose up -d
          docker-compose exec -T laravel.test composer install
      
      - name: Run migrations
        run: docker-compose exec -T laravel.test php artisan migrate --force
      
      - name: Run tests
        run: docker-compose exec -T laravel.test php artisan test --filter=Assignment
      
      - name: Check code style
        run: docker-compose exec -T laravel.test ./vendor/bin/pint --test
```

---

## Next Steps

1. **Read Planning Docs**:
   - [plan.md](plan.md) - Implementation phases
   - [research.md](research.md) - Technology decisions
   - [data-model.md](data-model.md) - Database schema

2. **Start with Phase 1**:
   - Run migrations
   - Add model casts and accessors
   - Write unit tests for model methods

3. **Build Graph Services** (Phase 2):
   - Implement `AssignmentFetcher`
   - Implement `GroupResolver`
   - Implement `ScopeTagResolver`
   - Write unit tests with mocked Graph responses

4. **Implement MVP** (Phase 3-4):
   - Add backup checkbox
   - Create assignment backup logic
   - Add Policy View assignments tab

---

## Additional Resources

- **Laravel Docs**: https://laravel.com/docs/12.x
- **Filament Docs**: https://filamentphp.com/docs/4.x
- **Graph API Docs**: https://learn.microsoft.com/en-us/graph/api/overview
- **Pest Docs**: https://pestphp.com/docs/

---

**Status**: Quickstart Complete  
**Next Document**: `tasks.md` (detailed task breakdown)
